diff options
| -rw-r--r-- | chime.el | 37 | ||||
| -rw-r--r-- | tests/test-chime--deduplicate-events-by-title.el | 53 |
2 files changed, 76 insertions, 14 deletions
@@ -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 |
