@@ -10,6 +10,40 @@ local ClockReport = require('orgmode.clock.report')
10
10
local utils = require (' orgmode.utils' )
11
11
local SortingStrategy = require (' orgmode.agenda.sorting_strategy' )
12
12
local 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
13
47
14
48
--- @class OrgAgendaTypeOpts
15
49
--- @field files OrgFiles
@@ -357,9 +391,10 @@ function OrgAgendaType:_build_line(agenda_item, metadata)
357
391
hl_group = priority_hl_group ,
358
392
}))
359
393
end
394
+ local add_markup = type (headline .node ) == ' function' and headline :node () ~= nil and headline or nil
360
395
line :add_token (AgendaLineToken :new ({
361
396
content = headline :get_title (),
362
- add_markup_to_headline = headline ,
397
+ add_markup_to_headline = add_markup ,
363
398
}))
364
399
if not self .remove_tags and # headline :get_tags () > 0 then
365
400
local tags_string = headline :tags_to_string ()
@@ -395,16 +430,100 @@ function OrgAgendaType:_get_agenda_days()
395
430
headline = headline ,
396
431
})
397
432
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
398
473
end
399
474
end
400
475
401
476
local headlines = {}
402
477
for _ , day in ipairs (dates ) do
403
478
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' )
404
481
405
482
for index , item in ipairs (headline_dates ) do
406
483
local headline = item .headline
407
484
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
408
527
if agenda_item .is_valid and self :_matches_filters (headline ) then
409
528
table.insert (headlines , headline )
410
529
table.insert (date .agenda_items , agenda_item )
@@ -413,6 +532,20 @@ function OrgAgendaType:_get_agenda_days()
413
532
end
414
533
end
415
534
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
+
416
549
date .agenda_items = self :_sort (date .agenda_items )
417
550
date .category_length = math.max (11 , date .category_length + 1 )
418
551
date .label_length = math.min (11 , date .label_length )
0 commit comments