Skip to content
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
62 changes: 62 additions & 0 deletions lua/orgmode/agenda/diary_headline.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---@class OrgDiaryHeadline
---@field file OrgFile
---@field _title string
local DiaryHeadline = {}
DiaryHeadline.__index = function(self, key)
local method = rawget(DiaryHeadline, key)
if method ~= nil then
return method
end
-- Return a no-op for any unimplemented headline interface methods
return function()
return nil
end
end

---@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
86 changes: 85 additions & 1 deletion lua/orgmode/agenda/types/agenda.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ 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')

---@param expr string
---@param day OrgDate
---@return OrgDate|nil event_date
---@return number|nil remind_days
local function _get_remind_event_date(expr, day)
local month, day_of_month, remind_days = DiarySexp.extract_remind_info(expr)
if not month or not day_of_month or not remind_days then
return nil, nil
end
return day:set({ month = month, day = day_of_month }), remind_days
end

---@alias OrgAgendaDay { day: OrgDate, agenda_items: OrgAgendaItem[], category_length: number, label_length: 0 }

Expand Down Expand Up @@ -532,9 +547,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 @@ -570,16 +586,83 @@ function OrgAgendaType:_get_agenda_days()
headline = headline,
})
end
-- Include diary sexp entries
for _, entry in ipairs(headline:get_diary_sexps()) do
local matcher = entry.expr and DiarySexp.parse(entry.expr) or nil
if matcher then
table.insert(headline_dates, {
headline_date = self.from:clone({ active = true, type = 'NONE' }),
headline = headline,
_diary_matcher = matcher,
})
end
end
end
-- Also include file-level diary sexp entries (outside headlines)
for _, entry in ipairs(orgfile:get_diary_sexps()) do
local matcher = entry.expr and DiarySexp.parse(entry.expr) or nil
if 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

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 matches = item._diary_matcher:matches(day)
-- 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 = _get_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 = _get_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 @@ -589,6 +672,7 @@ function OrgAgendaType:_get_agenda_days()
end

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

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
58 changes: 58 additions & 0 deletions lua/orgmode/diary/format.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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('%', 1, true) 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:gsub('%%d', tostring(age))
out = out:gsub('%%s', suff)
return out
end

return M
Loading
Loading