aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-05 12:16:37 -0500
committerCraig Jennings <c@cjennings.net>2026-05-05 12:16:37 -0500
commit44831f8587947212f2df8c38e81a3c1b6fa588fc (patch)
treeaf52e2ae3120977d0ac01a180f608f071b04cf50
parent8db6357d2e2f912ca382b259ea2675cf9058511e (diff)
downloadchime-44831f8587947212f2df8c38e81a3c1b6fa588fc.tar.gz
chime-44831f8587947212f2df8c38e81a3c1b6fa588fc.zip
fix: stop tooltip dedup from hiding distinct events sharing a title
`chime--deduplicate-events-by-title' was keying off the user-facing title alone, so two real "1:1" entries on different days collapsed to one and the second one disappeared from the tooltip. I changed the key to `(marker-file . marker-pos)' — the source heading identity — so distinct headings keep both copies and recurring expansions of one heading still fold to the soonest. When marker info is missing (synthesized test events, edge cases) the key falls back to title, which preserves the older fixtures the function was originally tested against. Three new tests cover the regression paths: two distinct headings sharing a title both survive, one heading expanded into multiple instances still collapses to the soonest, and a mix of sourced and unsourced events behaves correctly under both code paths.
-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