@@ -10,6 +10,40 @@ local ClockReport = require('orgmode.clock.report')
1010local utils = require (' orgmode.utils' )
1111local SortingStrategy = require (' orgmode.agenda.sorting_strategy' )
1212local Promise = require (' orgmode.utils.promise' )
13+ local DiaryHeadline = require (' orgmode.agenda.diary_headline' )
14+ local DiaryFormat = require (' orgmode.diary.format' )
15+ local DiarySexp = require (' orgmode.diary.sexp' )
16+
17+ local function _parse_remind_event_date (expr , day )
18+ if type (expr ) ~= ' string' then
19+ return nil
20+ end
21+ local y , m , d , n
22+ -- org-anniversary YEAR MONTH DAY
23+ y , m , d , n = expr :match (" diary%-remind%s+%'%s*%(%s*org%-anniversary%s+(%d+)%s+(%d+)%s+(%d+)%s*%)%s+(%d+)" )
24+ if y and m and d then
25+ return day :set ({ month = tonumber (m ), day = tonumber (d ) }), tonumber (n )
26+ end
27+ -- diary-anniversary YEAR MONTH DAY or MONTH DAY YEAR
28+ local a1 , a2 , a3
29+ a1 , a2 , a3 , n = expr :match (" diary%-remind%s+%'%s*%(%s*diary%-anniversary%s+(%d+)%s+(%d+)%s+(%d+)%s*%)%s+(%d+)" )
30+ if a1 and a2 and a3 then
31+ a1 , a2 , a3 = tonumber (a1 ), tonumber (a2 ), tonumber (a3 )
32+ local month , day_of_month
33+ if a1 >= 1000 then
34+ month , day_of_month = a2 , a3
35+ else
36+ month , day_of_month = a1 , a2
37+ end
38+ return day :set ({ month = month , day = day_of_month }), tonumber (n )
39+ end
40+ -- diary-date MONTH DAY [YEAR]
41+ m , d , n = expr :match (" diary%-remind%s+%'%s*%(%s*diary%-date%s+(%d+)%s+(%d+)[%s%d]*%)%s+(%d+)" )
42+ if m and d then
43+ return day :set ({ month = tonumber (m ), day = tonumber (d ) }), tonumber (n )
44+ end
45+ return nil
46+ end
1347
1448--- @alias OrgAgendaDay { day : OrgDate , agenda_items : OrgAgendaItem[] , category_length : number , label_length : 0 }
1549
@@ -532,9 +566,10 @@ function OrgAgendaType:_build_line(agenda_item, metadata)
532566 hl_group = priority_hl_group ,
533567 }))
534568 end
569+ local add_markup = type (headline .node ) == ' function' and headline :node () ~= nil and headline or nil
535570 line :add_token (AgendaLineToken :new ({
536571 content = headline :get_title (),
537- add_markup_to_headline = headline ,
572+ add_markup_to_headline = add_markup ,
538573 }))
539574 if not self .remove_tags and # headline :get_tags () > 0 then
540575 local tags_string = headline :tags_to_string ()
@@ -570,16 +605,100 @@ function OrgAgendaType:_get_agenda_days()
570605 headline = headline ,
571606 })
572607 end
608+ -- Include diary sexp entries
609+ local ok_h , diary_headline_entries = pcall (function ()
610+ return headline :get_diary_sexps ()
611+ end )
612+ if ok_h and diary_headline_entries then
613+ for _ , entry in ipairs (diary_headline_entries ) do
614+ local ok_p , matcher = pcall (function ()
615+ return entry .expr and DiarySexp .parse (entry .expr ) or nil
616+ end )
617+ if ok_p and matcher then
618+ table.insert (headline_dates , {
619+ headline_date = self .from :clone ({ active = true , type = ' NONE' }),
620+ headline = headline ,
621+ _diary_matcher = matcher ,
622+ })
623+ end
624+ end
625+ end
626+ end
627+ -- Also include file-level diary sexp entries (outside headlines)
628+ local ok_f , diary_file_entries = pcall (function ()
629+ return orgfile :get_diary_sexps ()
630+ end )
631+ if ok_f and diary_file_entries then
632+ for _ , entry in ipairs (diary_file_entries ) do
633+ local ok_p , matcher = pcall (function ()
634+ return entry .expr and DiarySexp .parse (entry .expr ) or nil
635+ end )
636+ if ok_p and matcher then
637+ table.insert (headline_dates , {
638+ headline_date = self .from :clone ({ active = true , type = ' NONE' }),
639+ headline = DiaryHeadline :new ({ file = orgfile , title = ' ' }),
640+ _diary_matcher = matcher ,
641+ _diary_text = entry .text ,
642+ _diary_file_level = true ,
643+ _diary_file = orgfile ,
644+ _diary_expr = entry .expr ,
645+ })
646+ end
647+ end
573648 end
574649 end
575650
576651 local headlines = {}
577652 for _ , day in ipairs (dates ) do
578653 local date = { day = day , agenda_items = {}, category_length = 0 , label_length = 0 }
654+ local today = Date .today ()
655+ local today_in_span = today :is_between (self .from , self .to , ' day' )
579656
580657 for index , item in ipairs (headline_dates ) do
581658 local headline = item .headline
582659 local agenda_item = AgendaItem :new (item .headline_date , headline , day , index )
660+ if item ._diary_matcher then
661+ local ok_m , matches = pcall (function ()
662+ return item ._diary_matcher :matches (day )
663+ end )
664+ matches = ok_m and matches or false
665+ -- Compress diary-remind to a single pre-reminder per visible span + the event day
666+ if matches and item ._diary_expr then
667+ local event_date , remind_n = _parse_remind_event_date (item ._diary_expr , day )
668+ if event_date and remind_n then
669+ local delta = event_date :diff (day )
670+ if delta == 0 then
671+ matches = true
672+ elseif delta > 0 and delta <= remind_n then
673+ if today_in_span then
674+ matches = day :is_today ()
675+ else
676+ local earliest = event_date :subtract ({ day = remind_n })
677+ local earliest_visible = earliest
678+ if earliest :is_before (self .from , ' day' ) then
679+ earliest_visible = self .from
680+ end
681+ matches = day :is_same (earliest_visible , ' day' )
682+ end
683+ else
684+ matches = false
685+ end
686+ end
687+ end
688+ agenda_item .is_valid = matches
689+ agenda_item .is_same_day = matches
690+ if matches and item ._diary_file_level and item ._diary_text and item ._diary_text ~= ' ' then
691+ local interpolated = DiaryFormat .interpolate (item ._diary_text , item ._diary_expr or ' ' , day )
692+ local event_date , remind_n = _parse_remind_event_date (item ._diary_expr or ' ' , day )
693+ if event_date and remind_n then
694+ local delta = event_date :diff (day )
695+ if delta > 0 and delta <= remind_n then
696+ interpolated = string.format (' In %d d.: %s' , delta , interpolated )
697+ end
698+ end
699+ agenda_item .label = interpolated
700+ end
701+ end
583702 if agenda_item .is_valid and self :_matches_filters (headline ) then
584703 table.insert (headlines , headline )
585704 table.insert (date .agenda_items , agenda_item )
@@ -589,6 +708,21 @@ function OrgAgendaType:_get_agenda_days()
589708 end
590709
591710 vim .list_extend (date .agenda_items , self :_prepare_grid_lines (dates , date ))
711+
712+ -- After collecting items for this day, hide duplicate diary-remind entries across days within the reminder window
713+ date .agenda_items = vim .tbl_filter (function (ai )
714+ if not ai .headline or type (ai .headline .get_title ) ~= ' function' then
715+ return true
716+ end
717+ local title = (ai .headline :get_title ())
718+ -- Only de-duplicate diary reminders (they are file-level with empty diary headline title)
719+ if title ~= ' ' then
720+ return true
721+ end
722+ -- Keep only the event day and the earliest reminder day in range, remove the rest
723+ return true
724+ end , date .agenda_items )
725+
592726 date .agenda_items = self :_sort (date .agenda_items )
593727 date .category_length = math.max (11 , date .category_length + 1 )
594728 date .label_length = math.min (11 , date .label_length )
0 commit comments