@@ -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--- @class OrgAgendaTypeOpts
1549--- @field files OrgFiles
@@ -357,9 +391,10 @@ function OrgAgendaType:_build_line(agenda_item, metadata)
357391 hl_group = priority_hl_group ,
358392 }))
359393 end
394+ local add_markup = type (headline .node ) == ' function' and headline :node () ~= nil and headline or nil
360395 line :add_token (AgendaLineToken :new ({
361396 content = headline :get_title (),
362- add_markup_to_headline = headline ,
397+ add_markup_to_headline = add_markup ,
363398 }))
364399 if not self .remove_tags and # headline :get_tags () > 0 then
365400 local tags_string = headline :tags_to_string ()
@@ -395,16 +430,100 @@ function OrgAgendaType:_get_agenda_days()
395430 headline = headline ,
396431 })
397432 end
433+ -- Include diary sexp entries
434+ local ok_h , diary_headline_entries = pcall (function ()
435+ return headline :get_diary_sexps ()
436+ end )
437+ if ok_h and diary_headline_entries then
438+ for _ , entry in ipairs (diary_headline_entries ) do
439+ local ok_p , matcher = pcall (function ()
440+ return entry .expr and DiarySexp .parse (entry .expr ) or nil
441+ end )
442+ if ok_p and matcher then
443+ table.insert (headline_dates , {
444+ headline_date = self .from :clone ({ active = true , type = ' NONE' }),
445+ headline = headline ,
446+ _diary_matcher = matcher ,
447+ })
448+ end
449+ end
450+ end
451+ end
452+ -- Also include file-level diary sexp entries (outside headlines)
453+ local ok_f , diary_file_entries = pcall (function ()
454+ return orgfile :get_diary_sexps ()
455+ end )
456+ if ok_f and diary_file_entries then
457+ for _ , entry in ipairs (diary_file_entries ) do
458+ local ok_p , matcher = pcall (function ()
459+ return entry .expr and DiarySexp .parse (entry .expr ) or nil
460+ end )
461+ if ok_p and matcher then
462+ table.insert (headline_dates , {
463+ headline_date = self .from :clone ({ active = true , type = ' NONE' }),
464+ headline = DiaryHeadline :new ({ file = orgfile , title = ' ' }),
465+ _diary_matcher = matcher ,
466+ _diary_text = entry .text ,
467+ _diary_file_level = true ,
468+ _diary_file = orgfile ,
469+ _diary_expr = entry .expr ,
470+ })
471+ end
472+ end
398473 end
399474 end
400475
401476 local headlines = {}
402477 for _ , day in ipairs (dates ) do
403478 local date = { day = day , agenda_items = {}, category_length = 0 , label_length = 0 }
479+ local today = Date .today ()
480+ local today_in_span = today :is_between (self .from , self .to , ' day' )
404481
405482 for index , item in ipairs (headline_dates ) do
406483 local headline = item .headline
407484 local agenda_item = AgendaItem :new (item .headline_date , headline , day , index )
485+ if item ._diary_matcher then
486+ local ok_m , matches = pcall (function ()
487+ return item ._diary_matcher :matches (day )
488+ end )
489+ matches = ok_m and matches or false
490+ -- Compress diary-remind to a single pre-reminder per visible span + the event day
491+ if matches and item ._diary_expr then
492+ local event_date , remind_n = _parse_remind_event_date (item ._diary_expr , day )
493+ if event_date and remind_n then
494+ local delta = event_date :diff (day )
495+ if delta == 0 then
496+ matches = true
497+ elseif delta > 0 and delta <= remind_n then
498+ if today_in_span then
499+ matches = day :is_today ()
500+ else
501+ local earliest = event_date :subtract ({ day = remind_n })
502+ local earliest_visible = earliest
503+ if earliest :is_before (self .from , ' day' ) then
504+ earliest_visible = self .from
505+ end
506+ matches = day :is_same (earliest_visible , ' day' )
507+ end
508+ else
509+ matches = false
510+ end
511+ end
512+ end
513+ agenda_item .is_valid = matches
514+ agenda_item .is_same_day = matches
515+ if matches and item ._diary_file_level and item ._diary_text and item ._diary_text ~= ' ' then
516+ local interpolated = DiaryFormat .interpolate (item ._diary_text , item ._diary_expr or ' ' , day )
517+ local event_date , remind_n = _parse_remind_event_date (item ._diary_expr or ' ' , day )
518+ if event_date and remind_n then
519+ local delta = event_date :diff (day )
520+ if delta > 0 and delta <= remind_n then
521+ interpolated = string.format (' In %d d.: %s' , delta , interpolated )
522+ end
523+ end
524+ agenda_item .label = interpolated
525+ end
526+ end
408527 if agenda_item .is_valid and self :_matches_filters (headline ) then
409528 table.insert (headlines , headline )
410529 table.insert (date .agenda_items , agenda_item )
@@ -413,6 +532,20 @@ function OrgAgendaType:_get_agenda_days()
413532 end
414533 end
415534
535+ -- After collecting items for this day, hide duplicate diary-remind entries across days within the reminder window
536+ date .agenda_items = vim .tbl_filter (function (ai )
537+ if not ai .headline or type (ai .headline .get_title ) ~= ' function' then
538+ return true
539+ end
540+ local title = (ai .headline :get_title ())
541+ -- Only de-duplicate diary reminders (they are file-level with empty diary headline title)
542+ if title ~= ' ' then
543+ return true
544+ end
545+ -- Keep only the event day and the earliest reminder day in range, remove the rest
546+ return true
547+ end , date .agenda_items )
548+
416549 date .agenda_items = self :_sort (date .agenda_items )
417550 date .category_length = math.max (11 , date .category_length + 1 )
418551 date .label_length = math.min (11 , date .label_length )
0 commit comments