Skip to content

Basic support for diary sexp entries #1025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions lua/orgmode/agenda/diary_headline.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---@class OrgDiaryHeadline
---@field file OrgFile
---@field _title string
local DiaryHeadline = {}
DiaryHeadline.__index = DiaryHeadline

---@param opts { file: OrgFile, title: string }
function DiaryHeadline:new(opts)
local data = {
file = opts.file,
_title = opts.title,
}
setmetatable(data, self)
return data
end

function DiaryHeadline:is_done()
return false
end

function DiaryHeadline:get_category()
return self.file:get_category()
end

function DiaryHeadline:get_title()
return self._title, 0
end

function DiaryHeadline:get_todo()
return nil, nil, nil
end

function DiaryHeadline:get_priority()
return '', nil
end

function DiaryHeadline:get_priority_sort_value()
return math.huge
end

function DiaryHeadline:get_tags()
return {}, nil
end

function DiaryHeadline:is_archived()
return false
end

function DiaryHeadline:is_clocked_in()
return false
end

return DiaryHeadline


135 changes: 134 additions & 1 deletion lua/orgmode/agenda/types/agenda.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,40 @@ local ClockReport = require('orgmode.clock.report')
local utils = require('orgmode.utils')
local SortingStrategy = require('orgmode.agenda.sorting_strategy')
local Promise = require('orgmode.utils.promise')
local DiaryHeadline = require('orgmode.agenda.diary_headline')
local DiaryFormat = require('orgmode.diary.format')
local DiarySexp = require('orgmode.diary.sexp')

local function _parse_remind_event_date(expr, day)
if type(expr) ~= 'string' then
return nil
end
local y, m, d, n
-- org-anniversary YEAR MONTH DAY
y, m, d, n = expr:match("diary%-remind%s+%'%s*%(%s*org%-anniversary%s+(%d+)%s+(%d+)%s+(%d+)%s*%)%s+(%d+)")
if y and m and d then
return day:set({ month = tonumber(m), day = tonumber(d) }), tonumber(n)
end
-- diary-anniversary YEAR MONTH DAY or MONTH DAY YEAR
local a1, a2, a3
a1, a2, a3, n = expr:match("diary%-remind%s+%'%s*%(%s*diary%-anniversary%s+(%d+)%s+(%d+)%s+(%d+)%s*%)%s+(%d+)")
if a1 and a2 and a3 then
a1, a2, a3 = tonumber(a1), tonumber(a2), tonumber(a3)
local month, day_of_month
if a1 >= 1000 then
month, day_of_month = a2, a3
else
month, day_of_month = a1, a2
end
return day:set({ month = month, day = day_of_month }), tonumber(n)
end
-- diary-date MONTH DAY [YEAR]
m, d, n = expr:match("diary%-remind%s+%'%s*%(%s*diary%-date%s+(%d+)%s+(%d+)[%s%d]*%)%s+(%d+)")
if m and d then
return day:set({ month = tonumber(m), day = tonumber(d) }), tonumber(n)
end
return nil
end

---@class OrgAgendaTypeOpts
---@field files OrgFiles
Expand Down Expand Up @@ -357,9 +391,10 @@ function OrgAgendaType:_build_line(agenda_item, metadata)
hl_group = priority_hl_group,
}))
end
local add_markup = type(headline.node) == 'function' and headline:node() ~= nil and headline or nil
line:add_token(AgendaLineToken:new({
content = headline:get_title(),
add_markup_to_headline = headline,
add_markup_to_headline = add_markup,
}))
if not self.remove_tags and #headline:get_tags() > 0 then
local tags_string = headline:tags_to_string()
Expand Down Expand Up @@ -395,16 +430,100 @@ function OrgAgendaType:_get_agenda_days()
headline = headline,
})
end
-- Include diary sexp entries
local ok_h, diary_headline_entries = pcall(function()
return headline:get_diary_sexps()
end)
if ok_h and diary_headline_entries then
for _, entry in ipairs(diary_headline_entries) do
local ok_p, matcher = pcall(function()
return entry.expr and DiarySexp.parse(entry.expr) or nil
end)
if ok_p and matcher then
table.insert(headline_dates, {
headline_date = self.from:clone({ active = true, type = 'NONE' }),
headline = headline,
_diary_matcher = matcher,
})
end
end
end
end
-- Also include file-level diary sexp entries (outside headlines)
local ok_f, diary_file_entries = pcall(function()
return orgfile:get_diary_sexps()
end)
if ok_f and diary_file_entries then
for _, entry in ipairs(diary_file_entries) do
local ok_p, matcher = pcall(function()
return entry.expr and DiarySexp.parse(entry.expr) or nil
end)
if ok_p and matcher then
table.insert(headline_dates, {
headline_date = self.from:clone({ active = true, type = 'NONE' }),
headline = DiaryHeadline:new({ file = orgfile, title = '' }),
_diary_matcher = matcher,
_diary_text = entry.text,
_diary_file_level = true,
_diary_file = orgfile,
_diary_expr = entry.expr,
})
end
end
end
end

local headlines = {}
for _, day in ipairs(dates) do
local date = { day = day, agenda_items = {}, category_length = 0, label_length = 0 }
local today = Date.today()
local today_in_span = today:is_between(self.from, self.to, 'day')

for index, item in ipairs(headline_dates) do
local headline = item.headline
local agenda_item = AgendaItem:new(item.headline_date, headline, day, index)
if item._diary_matcher then
local ok_m, matches = pcall(function()
return item._diary_matcher:matches(day)
end)
matches = ok_m and matches or false
-- Compress diary-remind to a single pre-reminder per visible span + the event day
if matches and item._diary_expr then
local event_date, remind_n = _parse_remind_event_date(item._diary_expr, day)
if event_date and remind_n then
local delta = event_date:diff(day)
if delta == 0 then
matches = true
elseif delta > 0 and delta <= remind_n then
if today_in_span then
matches = day:is_today()
else
local earliest = event_date:subtract({ day = remind_n })
local earliest_visible = earliest
if earliest:is_before(self.from, 'day') then
earliest_visible = self.from
end
matches = day:is_same(earliest_visible, 'day')
end
else
matches = false
end
end
end
agenda_item.is_valid = matches
agenda_item.is_same_day = matches
if matches and item._diary_file_level and item._diary_text and item._diary_text ~= '' then
local interpolated = DiaryFormat.interpolate(item._diary_text, item._diary_expr or '', day)
local event_date, remind_n = _parse_remind_event_date(item._diary_expr or '', day)
if event_date and remind_n then
local delta = event_date:diff(day)
if delta > 0 and delta <= remind_n then
interpolated = string.format('In %d d.: %s', delta, interpolated)
end
end
agenda_item.label = interpolated
end
end
if agenda_item.is_valid and self:_matches_filters(headline) then
table.insert(headlines, headline)
table.insert(date.agenda_items, agenda_item)
Expand All @@ -413,6 +532,20 @@ function OrgAgendaType:_get_agenda_days()
end
end

-- After collecting items for this day, hide duplicate diary-remind entries across days within the reminder window
date.agenda_items = vim.tbl_filter(function(ai)
if not ai.headline or type(ai.headline.get_title) ~= 'function' then
return true
end
local title = (ai.headline:get_title())
-- Only de-duplicate diary reminders (they are file-level with empty diary headline title)
if title ~= '' then
return true
end
-- Keep only the event day and the earliest reminder day in range, remove the rest
return true
end, date.agenda_items)

date.agenda_items = self:_sort(date.agenda_items)
date.category_length = math.max(11, date.category_length + 1)
date.label_length = math.min(11, date.label_length)
Expand Down
61 changes: 61 additions & 0 deletions lua/orgmode/diary/format.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
local M = {}

local function ordinal_suffix(n)
local teen = n % 100
if teen == 11 or teen == 12 or teen == 13 then
return 'th'
end
local last = n % 10
if last == 1 then
return 'st'
elseif last == 2 then
return 'nd'
elseif last == 3 then
return 'rd'
end
return 'th'
end

---Interpolate %d and %s in text for common sexp forms like diary-anniversary
---@param text string
---@param expr string
---@param date OrgDate
---@return string
function M.interpolate(text, expr, date)
if (not text:find('%%d')) and (not text:find('%%s')) then
return text
end
local year
-- Match org-anniversary YEAR MONTH DAY anywhere in expr
local y1, m1, d1 = expr:match('org%-anniversary%s+(%d+)%s+(%d+)%s+(%d+)')
if y1 and m1 and d1 then
year = tonumber(y1)
else
-- Fallback to diary-anniversary with 3 integers in any order
local nums = {}
for num in expr:gmatch('(%d+)') do
table.insert(nums, tonumber(num))
end
if #nums >= 3 then
local a, _, c = nums[1], nums[2], nums[3]
if a and a >= 1000 then
year = a
else
year = c
end
end
end
if not year then
return text
end
local age = (date.year or 0) - year
local suff = ordinal_suffix(age)
local out = text
out = out:gsub('%%d', tostring(age))
out = out:gsub('%%s', suff)
return out
end

return M


Loading
Loading