aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--chime.el37
-rw-r--r--tests/test-chime--deduplicate-events-by-title.el53
2 files changed, 76 insertions, 14 deletions
diff --git a/chime.el b/chime.el
index 2bc1b75..3dafc48 100644
--- a/chime.el
+++ b/chime.el
@@ -1079,27 +1079,36 @@ Left-click opens calendar URL (if set), right-click jumps to event."
'local-map map))))
(defun chime--deduplicate-events-by-title (upcoming-events)
- "Deduplicate UPCOMING-EVENTS by title, keeping soonest occurrence.
+ "Collapse UPCOMING-EVENTS that come from the same source heading.
UPCOMING-EVENTS should be a list where each element is
-\(EVENT TIME-INFO MINUTES).
-Returns a new list with only the soonest occurrence of each
-unique title.
-
-This prevents recurring events from appearing multiple times in
-the tooltip when `org-agenda-list' expands them into separate
-event objects."
- (let ((title-hash (make-hash-table :test 'equal)))
+\(EVENT TIME-INFO MINUTES). Returns a new list with one entry per
+source heading, keeping the soonest occurrence.
+
+The dedup key is the heading's marker (`marker-file' + `marker-pos'
+on the event alist) so two distinct headings sharing a display title
+both survive — for example, two separate \"1:1\" entries on different
+days. When marker info is missing (typically synthesized test events),
+the key falls back to the title so older callers and fixtures keep
+working.
+
+The function still earns its keep against `org-agenda-list', which
+expands a recurring entry into multiple instances all sharing one
+marker; those collapse to a single soonest tooltip line."
+ (let ((id-hash (make-hash-table :test 'equal)))
(dolist (item upcoming-events)
(let* ((event (car item))
- (title (cdr (assoc 'title event)))
(minutes (caddr item))
- (existing (gethash title title-hash)))
- ;; Only keep if this is the first occurrence or soonest so far
+ (marker-file (cdr (assoc 'marker-file event)))
+ (marker-pos (cdr (assoc 'marker-pos event)))
+ (key (if (and marker-file marker-pos)
+ (cons marker-file marker-pos)
+ (cdr (assoc 'title event))))
+ (existing (gethash key id-hash)))
(when (or (not existing)
(< minutes (caddr existing)))
- (puthash title item title-hash))))
- (hash-table-values title-hash)))
+ (puthash key item id-hash))))
+ (hash-table-values id-hash)))
(defun chime--find-soonest-time-in-window (times now lookahead-minutes)
"Find soonest time from TIMES list within LOOKAHEAD-MINUTES from NOW.
diff --git a/tests/test-chime--deduplicate-events-by-title.el b/tests/test-chime--deduplicate-events-by-title.el
index 69bb632..66ec37a 100644
--- a/tests/test-chime--deduplicate-events-by-title.el
+++ b/tests/test-chime--deduplicate-events-by-title.el
@@ -47,6 +47,15 @@ Returns format: (EVENT TIME-INFO MINUTES)"
'("dummy-time-string" . nil) ; TIME-INFO (not used in deduplication)
minutes))
+(defun test-make-upcoming-item-with-source (title minutes file pos)
+ "Build an upcoming-events item carrying source-heading identity.
+TITLE / MINUTES match `test-make-upcoming-item'; FILE and POS attach
+`marker-file' and `marker-pos' to the event alist so the dedup key
+can use heading identity instead of title."
+ (list `((title . ,title) (marker-file . ,file) (marker-pos . ,pos))
+ '("dummy-time-string" . nil)
+ minutes))
+
;;; Normal Cases
(ert-deftest test-chime--deduplicate-events-by-title-normal-recurring-daily-keeps-soonest ()
@@ -183,5 +192,49 @@ One instance should be kept."
(let ((result (chime--deduplicate-events-by-title nil)))
(should (null result))))
+;;; Source-heading Cases (distinct headings with shared title)
+
+(ert-deftest test-chime--deduplicate-events-by-title-distinct-headings-same-title-both-kept ()
+ "Two events that share a title but live at different markers must both
+survive the dedup pass. Recurring-event collapse keys off the source
+heading (file + position), not the user-facing title."
+ (let* ((events (list
+ (test-make-upcoming-item-with-source
+ "1:1" 30 "/work.org" 100)
+ (test-make-upcoming-item-with-source
+ "1:1" 60 "/work.org" 500)))
+ (result (chime--deduplicate-events-by-title events)))
+ (should (= 2 (length result)))))
+
+(ert-deftest test-chime--deduplicate-events-by-title-recurring-same-marker-collapsed ()
+ "Multiple expansions of one recurring entry share the same marker
+and should still collapse to the soonest occurrence."
+ (let* ((events (list
+ (test-make-upcoming-item-with-source
+ "Standup" 60 "/work.org" 100)
+ (test-make-upcoming-item-with-source
+ "Standup" 1500 "/work.org" 100)
+ (test-make-upcoming-item-with-source
+ "Standup" 2940 "/work.org" 100)))
+ (result (chime--deduplicate-events-by-title events)))
+ (should (= 1 (length result)))
+ (should (= 60 (caddr (car result))))))
+
+(ert-deftest test-chime--deduplicate-events-by-title-mixed-source-and-title-fallback ()
+ "Sourced and unsourced events coexist: sourced ones key off the marker,
+unsourced ones still collapse by title."
+ (let* ((events (list
+ ;; Two distinct headings sharing a title
+ (test-make-upcoming-item-with-source
+ "Sync" 30 "/a.org" 100)
+ (test-make-upcoming-item-with-source
+ "Sync" 90 "/b.org" 200)
+ ;; Test event without marker info — fallback to title
+ (test-make-upcoming-item "Standalone" 45)
+ (test-make-upcoming-item "Standalone" 600)))
+ (result (chime--deduplicate-events-by-title events)))
+ ;; Two Syncs (distinct headings) plus one Standalone (title-collapsed)
+ (should (= 3 (length result)))))
+
(provide 'test-chime--deduplicate-events-by-title)
;;; test-chime--deduplicate-events-by-title.el ends here