Skip to content

Commit 7ab2d88

Browse files
committed
Support diary sexp entries
1 parent 9deee54 commit 7ab2d88

File tree

7 files changed

+974
-1
lines changed

7 files changed

+974
-1
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---@class OrgDiaryHeadline
2+
---@field file OrgFile
3+
---@field _title string
4+
local DiaryHeadline = {}
5+
DiaryHeadline.__index = DiaryHeadline
6+
7+
---@param opts { file: OrgFile, title: string }
8+
function DiaryHeadline:new(opts)
9+
local data = {
10+
file = opts.file,
11+
_title = opts.title,
12+
}
13+
setmetatable(data, self)
14+
return data
15+
end
16+
17+
function DiaryHeadline:is_done()
18+
return false
19+
end
20+
21+
function DiaryHeadline:get_category()
22+
return self.file:get_category()
23+
end
24+
25+
function DiaryHeadline:get_title()
26+
return self._title, 0
27+
end
28+
29+
function DiaryHeadline:get_todo()
30+
return nil, nil, nil
31+
end
32+
33+
function DiaryHeadline:get_priority()
34+
return '', nil
35+
end
36+
37+
function DiaryHeadline:get_priority_sort_value()
38+
return math.huge
39+
end
40+
41+
function DiaryHeadline:get_tags()
42+
return {}, nil
43+
end
44+
45+
function DiaryHeadline:is_archived()
46+
return false
47+
end
48+
49+
function DiaryHeadline:is_clocked_in()
50+
return false
51+
end
52+
53+
return DiaryHeadline
54+
55+

lua/orgmode/agenda/types/agenda.lua

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,40 @@ local ClockReport = require('orgmode.clock.report')
1010
local utils = require('orgmode.utils')
1111
local SortingStrategy = require('orgmode.agenda.sorting_strategy')
1212
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
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)

lua/orgmode/diary/format.lua

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
local M = {}
2+
3+
local function ordinal_suffix(n)
4+
local teen = n % 100
5+
if teen == 11 or teen == 12 or teen == 13 then
6+
return 'th'
7+
end
8+
local last = n % 10
9+
if last == 1 then
10+
return 'st'
11+
elseif last == 2 then
12+
return 'nd'
13+
elseif last == 3 then
14+
return 'rd'
15+
end
16+
return 'th'
17+
end
18+
19+
---Interpolate %d and %s in text for common sexp forms like diary-anniversary
20+
---@param text string
21+
---@param expr string
22+
---@param date OrgDate
23+
---@return string
24+
function M.interpolate(text, expr, date)
25+
if (not text:find('%%d')) and (not text:find('%%s')) then
26+
return text
27+
end
28+
local year
29+
-- Match org-anniversary YEAR MONTH DAY anywhere in expr
30+
local y1, m1, d1 = expr:match('org%-anniversary%s+(%d+)%s+(%d+)%s+(%d+)')
31+
if y1 and m1 and d1 then
32+
year = tonumber(y1)
33+
else
34+
-- Fallback to diary-anniversary with 3 integers in any order
35+
local nums = {}
36+
for num in expr:gmatch('(%d+)') do
37+
table.insert(nums, tonumber(num))
38+
end
39+
if #nums >= 3 then
40+
local a, _, c = nums[1], nums[2], nums[3]
41+
if a and a >= 1000 then
42+
year = a
43+
else
44+
year = c
45+
end
46+
end
47+
end
48+
if not year then
49+
return text
50+
end
51+
local age = (date.year or 0) - year
52+
local suff = ordinal_suffix(age)
53+
local out = text
54+
out = out:gsub('%%d', tostring(age))
55+
out = out:gsub('%%s', suff)
56+
return out
57+
end
58+
59+
return M
60+
61+

0 commit comments

Comments
 (0)