Skip to content

Commit a2fae2b

Browse files
committed
Switch to the latest tip
1 parent 7ab2d88 commit a2fae2b

File tree

7 files changed

+277
-115
lines changed

7 files changed

+277
-115
lines changed

lua/orgmode/agenda/diary_headline.lua

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
---@field file OrgFile
33
---@field _title string
44
local DiaryHeadline = {}
5-
DiaryHeadline.__index = DiaryHeadline
5+
DiaryHeadline.__index = function(self, key)
6+
local method = rawget(DiaryHeadline, key)
7+
if method ~= nil then
8+
return method
9+
end
10+
-- Return a no-op for any unimplemented headline interface methods
11+
return function()
12+
return nil
13+
end
14+
end
615

716
---@param opts { file: OrgFile, title: string }
817
function DiaryHeadline:new(opts)

lua/orgmode/agenda/types/agenda.lua

Lines changed: 32 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,16 @@ local DiaryHeadline = require('orgmode.agenda.diary_headline')
1414
local DiaryFormat = require('orgmode.diary.format')
1515
local DiarySexp = require('orgmode.diary.sexp')
1616

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
17+
---@param expr string
18+
---@param day OrgDate
19+
---@return OrgDate|nil event_date
20+
---@return number|nil remind_days
21+
local function _get_remind_event_date(expr, day)
22+
local month, day_of_month, remind_days = DiarySexp.extract_remind_info(expr)
23+
if not month or not day_of_month or not remind_days then
24+
return nil, nil
25+
end
26+
return day:set({ month = month, day = day_of_month }), remind_days
4627
end
4728

4829
---@alias OrgAgendaDay { day: OrgDate, agenda_items: OrgAgendaItem[], category_length: number, label_length: 0 }
@@ -606,46 +587,32 @@ function OrgAgendaType:_get_agenda_days()
606587
})
607588
end
608589
-- 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
590+
for _, entry in ipairs(headline:get_diary_sexps()) do
591+
local matcher = entry.expr and DiarySexp.parse(entry.expr) or nil
592+
if matcher then
637593
table.insert(headline_dates, {
638594
headline_date = self.from:clone({ active = true, type = 'NONE' }),
639-
headline = DiaryHeadline:new({ file = orgfile, title = '' }),
595+
headline = headline,
640596
_diary_matcher = matcher,
641-
_diary_text = entry.text,
642-
_diary_file_level = true,
643-
_diary_file = orgfile,
644-
_diary_expr = entry.expr,
645597
})
646598
end
647599
end
648600
end
601+
-- Also include file-level diary sexp entries (outside headlines)
602+
for _, entry in ipairs(orgfile:get_diary_sexps()) do
603+
local matcher = entry.expr and DiarySexp.parse(entry.expr) or nil
604+
if matcher then
605+
table.insert(headline_dates, {
606+
headline_date = self.from:clone({ active = true, type = 'NONE' }),
607+
headline = DiaryHeadline:new({ file = orgfile, title = '' }),
608+
_diary_matcher = matcher,
609+
_diary_text = entry.text,
610+
_diary_file_level = true,
611+
_diary_file = orgfile,
612+
_diary_expr = entry.expr,
613+
})
614+
end
615+
end
649616
end
650617

651618
local headlines = {}
@@ -658,13 +625,10 @@ function OrgAgendaType:_get_agenda_days()
658625
local headline = item.headline
659626
local agenda_item = AgendaItem:new(item.headline_date, headline, day, index)
660627
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
628+
local matches = item._diary_matcher:matches(day)
665629
-- Compress diary-remind to a single pre-reminder per visible span + the event day
666630
if matches and item._diary_expr then
667-
local event_date, remind_n = _parse_remind_event_date(item._diary_expr, day)
631+
local event_date, remind_n = _get_remind_event_date(item._diary_expr, day)
668632
if event_date and remind_n then
669633
local delta = event_date:diff(day)
670634
if delta == 0 then
@@ -689,7 +653,7 @@ function OrgAgendaType:_get_agenda_days()
689653
agenda_item.is_same_day = matches
690654
if matches and item._diary_file_level and item._diary_text and item._diary_text ~= '' then
691655
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)
656+
local event_date, remind_n = _get_remind_event_date(item._diary_expr or '', day)
693657
if event_date and remind_n then
694658
local delta = event_date:diff(day)
695659
if delta > 0 and delta <= remind_n then
@@ -709,20 +673,6 @@ function OrgAgendaType:_get_agenda_days()
709673

710674
vim.list_extend(date.agenda_items, self:_prepare_grid_lines(dates, date))
711675

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-
726676
date.agenda_items = self:_sort(date.agenda_items)
727677
date.category_length = math.max(11, date.category_length + 1)
728678
date.label_length = math.min(11, date.label_length)

lua/orgmode/diary/format.lua

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ end
2222
---@param date OrgDate
2323
---@return string
2424
function M.interpolate(text, expr, date)
25-
if (not text:find('%%d')) and (not text:find('%%s')) then
25+
if not text:find('%', 1, true) then
2626
return text
2727
end
2828
local year
@@ -50,8 +50,7 @@ function M.interpolate(text, expr, date)
5050
end
5151
local age = (date.year or 0) - year
5252
local suff = ordinal_suffix(age)
53-
local out = text
54-
out = out:gsub('%%d', tostring(age))
53+
local out = text:gsub('%%d', tostring(age))
5554
out = out:gsub('%%s', suff)
5655
return out
5756
end

lua/orgmode/diary/sexp.lua

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
local utils = require('orgmode.utils')
2-
31
---@class OrgDiarySexp
42
---@field _eval fun(self: OrgDiarySexp, date: OrgDate): boolean
53
local OrgDiarySexp = {}
@@ -103,14 +101,7 @@ local function parse_expr(tokens, idx)
103101
end
104102
end
105103

106-
---@param sexp string
107-
---@return any|nil
108-
local function normalize_sexp(sexp)
109-
return sexp
110-
end
111-
112104
local function parse(sexp)
113-
sexp = normalize_sexp(sexp)
114105
local tokens = tokenize(sexp)
115106
local expr, next_idx = parse_expr(tokens, 1)
116107
if not expr or next_idx <= 1 then
@@ -301,23 +292,24 @@ local function eval(ast, date)
301292
-- Fast path for supported anniversary/date forms
302293
local inner_op = inner[1]
303294
if inner_op == 'org-anniversary' or inner_op == 'diary-anniversary' or inner_op == 'diary-date' then
295+
local vars = build_variables(date)
304296
local month, day_of_month
305297
if inner_op == 'org-anniversary' then
306-
month = tonumber(resolve(inner[3], build_variables(date)))
307-
day_of_month = tonumber(resolve(inner[4], build_variables(date)))
298+
month = tonumber(resolve(inner[3], vars))
299+
day_of_month = tonumber(resolve(inner[4], vars))
308300
elseif inner_op == 'diary-anniversary' then
309301
-- (year month day) or (month day year)
310-
local a1 = tonumber(resolve(inner[2], build_variables(date)))
311-
local a2 = tonumber(resolve(inner[3], build_variables(date)))
312-
local a3 = tonumber(resolve(inner[4], build_variables(date)))
302+
local a1 = tonumber(resolve(inner[2], vars))
303+
local a2 = tonumber(resolve(inner[3], vars))
304+
local a3 = tonumber(resolve(inner[4], vars))
313305
if a1 and a1 >= 1000 then
314306
month, day_of_month = a2, a3
315307
else
316308
month, day_of_month = a1, a2
317309
end
318310
else -- diary-date month day [year]
319-
month = tonumber(resolve(inner[2], build_variables(date)))
320-
day_of_month = tonumber(resolve(inner[3], build_variables(date)))
311+
month = tonumber(resolve(inner[2], vars))
312+
day_of_month = tonumber(resolve(inner[3], vars))
321313
end
322314
if not month or not day_of_month then
323315
return false
@@ -361,8 +353,22 @@ local function eval(ast, date)
361353
else
362354
first_target_day = 1 + (7 - (first_wday - dow))
363355
end
364-
local candidate_day = first_target_day + (nth - 1) * 7
365-
return date.day == candidate_day
356+
if nth > 0 then
357+
local candidate_day = first_target_day + (nth - 1) * 7
358+
return date.day == candidate_day
359+
else
360+
-- Negative nth: count from end of month
361+
local last_day = date:last_day_of_month().day
362+
local last_target_day = first_target_day
363+
while last_target_day + 7 <= last_day do
364+
last_target_day = last_target_day + 7
365+
end
366+
local candidate_day = last_target_day + (nth + 1) * 7
367+
if candidate_day < 1 then
368+
return false
369+
end
370+
return date.day == candidate_day
371+
end
366372
end
367373

368374
-- Unknown operator: don't match
@@ -394,6 +400,51 @@ local function compile(expr)
394400
return OrgDiarySexp:new(matcher, expr)
395401
end
396402

403+
---Extract event date (month, day) and reminder window from a diary-remind AST.
404+
---@param ast any
405+
---@return number|nil month
406+
---@return number|nil day_of_month
407+
---@return number|nil remind_days
408+
local function extract_remind_info(ast)
409+
if type(ast) ~= 'table' or ast[1] ~= 'diary-remind' then
410+
return nil, nil, nil
411+
end
412+
local inner = ast[2]
413+
if type(inner) == 'table' and inner[1] == 'quote' then
414+
inner = inner[2]
415+
end
416+
if type(inner) ~= 'table' then
417+
return nil, nil, nil
418+
end
419+
local days_arg = ast[3]
420+
local days = type(days_arg) == 'number' and days_arg or tonumber(days_arg)
421+
if not days then
422+
return nil, nil, nil
423+
end
424+
local inner_op = inner[1]
425+
local month, day_of_month
426+
if inner_op == 'org-anniversary' then
427+
month = tonumber(inner[3])
428+
day_of_month = tonumber(inner[4])
429+
elseif inner_op == 'diary-anniversary' then
430+
local a1 = tonumber(inner[2])
431+
local a2 = tonumber(inner[3])
432+
local a3 = tonumber(inner[4])
433+
if a1 and a1 >= 1000 then
434+
month, day_of_month = a2, a3
435+
else
436+
month, day_of_month = a1, a2
437+
end
438+
elseif inner_op == 'diary-date' then
439+
month = tonumber(inner[2])
440+
day_of_month = tonumber(inner[3])
441+
end
442+
if not month or not day_of_month then
443+
return nil, nil, nil
444+
end
445+
return month, day_of_month, days
446+
end
447+
397448
local M = {}
398449

399450
---@param expr string
@@ -409,6 +460,26 @@ function M.parse(expr)
409460
return compile(trimmed)
410461
end
411462

463+
---Extract event month/day and reminder days from a diary-remind expression string.
464+
---@param expr string
465+
---@return number|nil month
466+
---@return number|nil day_of_month
467+
---@return number|nil remind_days
468+
function M.extract_remind_info(expr)
469+
if type(expr) ~= 'string' then
470+
return nil, nil, nil
471+
end
472+
local trimmed = vim.trim(expr)
473+
if not trimmed:match('^%(') then
474+
trimmed = '(' .. trimmed .. ')'
475+
end
476+
local ok, ast = pcall(parse, trimmed)
477+
if not ok or not ast then
478+
return nil, nil, nil
479+
end
480+
return extract_remind_info(ast)
481+
end
482+
412483
return M
413484

414485

lua/orgmode/files/file.lua

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -838,14 +838,21 @@ function OrgFile:get_diary_sexps()
838838
self:parse(true)
839839
local entries = {}
840840
for i, line in ipairs(self.lines) do
841-
-- Avoid timestamps like <%%(...)> or [%%(...)]
842-
if line:find('%%(', 1, true) and not line:find('<%%(', 1, true) and not line:find('[%%(', 1, true) then
841+
if line:find('%%(', 1, true) then
843842
local search_from = 1
844843
while true do
845844
local start_idx = line:find('%%(', search_from, true)
846845
if not start_idx then
847846
break
848847
end
848+
-- Skip timestamp-wrapped sexps like <%%(...)> or [%%(...)]
849+
if start_idx > 1 then
850+
local prev = line:sub(start_idx - 1, start_idx - 1)
851+
if prev == '<' or prev == '[' then
852+
search_from = start_idx + 3
853+
goto continue
854+
end
855+
end
849856
local expr_start = start_idx + 3 -- after "%%("
850857
local depth = 1
851858
local j = expr_start
@@ -874,6 +881,7 @@ function OrgFile:get_diary_sexps()
874881
range = Range.from_line(i),
875882
})
876883
search_from = close_idx + 1
884+
::continue::
877885
end
878886
end
879887
end

0 commit comments

Comments
 (0)