Skip to content

Commit

Permalink
feat(agenda)!: rewrite agenda rendering and fix filters (#848)
Browse files Browse the repository at this point in the history
  • Loading branch information
kristijanhusak authored Jan 10, 2025
1 parent 738de39 commit 8667167
Show file tree
Hide file tree
Showing 26 changed files with 1,342 additions and 1,191 deletions.
47 changes: 12 additions & 35 deletions lua/orgmode/agenda/agenda_item.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ end
---@field is_in_date_range boolean
---@field date_range_days number
---@field label string
---@field highlights table[]
local AgendaItem = {}

---@param headline_date OrgDate single date in a headline
---@param headline OrgHeadline
---@param date OrgDate date for which item should be rendered
---@param index? number
---@return OrgAgendaItem
function AgendaItem:new(headline_date, headline, date, index)
local opts = {}
opts.headline_date = headline_date
Expand All @@ -45,7 +45,6 @@ function AgendaItem:new(headline_date, headline, date, index)
opts.is_in_date_range = headline_date:is_none() and headline_date:is_in_date_range(date)
opts.date_range_days = headline_date:get_date_range_days()
opts.label = ''
opts.highlights = {}
if opts.repeats_on_date then
opts.real_date = opts.headline_date:apply_repeater_until(opts.date)
end
Expand Down Expand Up @@ -77,13 +76,6 @@ end

function AgendaItem:_generate_data()
self.label = self:_generate_label()
self.highlights = {}
local highlight = self:_generate_highlight()
if highlight then
table.insert(self.highlights, highlight)
end
self:_add_keyword_highlight()
self:_add_priority_highlight()
end

function AgendaItem:_is_valid_for_today()
Expand Down Expand Up @@ -220,63 +212,48 @@ function AgendaItem:_format_time(date)
return formatted_time
end

function AgendaItem:_generate_highlight()
---@return string | nil
function AgendaItem:get_hlgroup()
if self.headline_date:is_deadline() then
if self.headline:is_done() then
return { hlgroup = hl_map.ok }
return hl_map.ok
end
if self.is_today and self.headline_date:is_after(self.date, 'day') then
local diff = math.abs(self.date:diff(self.headline_date))
if diff <= FUTURE_DEADLINE_AS_WARNING_DAYS then
return { hlgroup = hl_map.warning }
return hl_map.warning
end
return nil
end

return { hlgroup = hl_map.deadline }
return hl_map.deadline
end

if self.headline_date:is_scheduled() then
if self.headline_date:is_past('day') and not self.headline:is_done() then
return { hlgroup = hl_map.warning }
return hl_map.warning
end

return { hlgroup = hl_map.ok }
return hl_map.ok
end

return nil
end

function AgendaItem:_add_keyword_highlight()
function AgendaItem:get_todo_hlgroup()
local todo_keyword, _, type = self.headline:get_todo()
if not todo_keyword then
return
end
local hlgroup = hl_map[todo_keyword] or hl_map[type]
if hlgroup then
table.insert(self.highlights, {
hlgroup = hlgroup,
todo_keyword = todo_keyword,
})
end
return hl_map[todo_keyword] or hl_map[type], todo_keyword
end

function AgendaItem:_add_priority_highlight()
function AgendaItem:get_priority_hlgroup()
local priority, priority_node = self.headline:get_priority()
if not priority_node then
return
end
local hlgroup = hl_map.priority[priority].hl_group
local last_hl = self.highlights[#self.highlights]
local start_col = 2
if last_hl and last_hl.todo_keyword then
start_col = start_col + last_hl.todo_keyword:len()
end
table.insert(self.highlights, {
hlgroup = hlgroup,
priority = priority,
start_col = start_col,
})
return hl_map.priority[priority].hl_group, priority
end

return AgendaItem
136 changes: 32 additions & 104 deletions lua/orgmode/agenda/filter.lua
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
local utils = require('orgmode.utils')
---@class OrgAgendaFilter
---@field value string
---@field available_tags table<string, boolean>
---@field available_categories table<string, boolean>
---@field filter_type 'include' | 'exclude'
---@field tags table[]
---@field categories table[]
---@field available_values table<string, boolean>
---@field values table[]
---@field term string
---@field parsed boolean
---@field applying boolean
local AgendaFilter = {}

---@return OrgAgendaFilter
function AgendaFilter:new()
local data = {
value = '',
available_tags = {},
available_categories = {},
filter_type = 'exclude',
tags = {},
categories = {},
available_values = {},
values = {},
term = '',
parsed = false,
applying = false,
}
setmetatable(data, self)
self.__index = self
Expand All @@ -40,156 +32,92 @@ function AgendaFilter:matches(headline)
return true
end
local term_match = vim.trim(self.term) == ''
local tag_cat_match_empty = #self.tags == 0 and #self.categories == 0
local values_match_empty = #self.values == 0

if not term_match then
local rgx = vim.regex(self.term) --[[@as vim.regex]]
term_match = rgx:match_str(headline:get_title()) and true or false
end

if tag_cat_match_empty then
if values_match_empty then
return term_match
end

local tag_cat_match = false

if self.filter_type == 'include' then
tag_cat_match = self:_matches_include(headline)
else
tag_cat_match = self:_matches_exclude(headline)
end
local tag_cat_match = self:_match(headline)

return tag_cat_match and term_match
end

---@param headline OrgHeadline
---@private
function AgendaFilter:_matches_exclude(headline)
for _, tag in ipairs(self.tags) do
if headline:has_tag(tag.value) then
return false
end
end

for _, category in ipairs(self.categories) do
if headline:matches_category(category.value) then
return false
end
end

return true
end

---@param headline OrgHeadline
---@private
function AgendaFilter:_matches_include(headline)
local tags_to_check = {}
local categories_to_check = {}

for _, tag in ipairs(self.tags) do
if tag.operator == '-' then
if headline:has_tag(tag.value) then
return false
end
else
table.insert(tags_to_check, tag.value)
end
end

for _, category in ipairs(self.categories) do
if category.operator == '-' then
if headline:matches_category(category.value) then
---@return boolean
function AgendaFilter:_match(headline)
for _, value in ipairs(self.values) do
if value.operator == '-' then
if headline:has_tag(value.value) or headline:matches_category(value.value) then
return false
end
else
table.insert(categories_to_check, category.value)
end
end

local tags_passed = #tags_to_check == 0
local categories_passed = #categories_to_check == 0

for _, category in ipairs(categories_to_check) do
if headline:matches_category(category) then
categories_passed = true
break
end
end

for _, tag in ipairs(tags_to_check) do
if headline:has_tag(tag) then
tags_passed = true
break
elseif not headline:has_tag(value.value) and not headline:matches_category(value.value) then
return false
end
end

return tags_passed and categories_passed
return true
end

---@param filter string
---@param skip_check? boolean do not check if given values exist in the current view
function AgendaFilter:parse(filter, skip_check)
filter = filter or ''
self.value = filter
self.tags = {}
self.categories = {}
self.values = {}
local search_rgx = '/[^/]*/?'
local search_term = filter:match(search_rgx)
if search_term then
search_term = search_term:gsub('^/*', ''):gsub('/*$', '')
end
filter = filter:gsub(search_rgx, '')
for operator, tag_cat in string.gmatch(filter, '([%+%-]*)([^%-%+]+)') do
if not operator or operator == '' or operator == '+' then
self.filter_type = 'include'
end
local val = vim.trim(tag_cat)
if val ~= '' then
if self.available_tags[val] or skip_check then
table.insert(self.tags, { operator = operator, value = val })
elseif self.available_categories[val] or skip_check then
table.insert(self.categories, { operator = operator, value = val })
if self.available_values[val] or skip_check then
table.insert(self.values, { operator = operator, value = val })
end
end
end
self.term = search_term or ''
self.applying = true
if skip_check then
self.parsed = true
end
return self
end

function AgendaFilter:reset()
self.value = ''
self.term = ''
self.parsed = false
self.applying = false
end

---@param content table[]
function AgendaFilter:parse_tags_and_categories(content)
---@param agenda_views OrgAgendaViewType[]
function AgendaFilter:parse_available_filters(agenda_views)
if self.parsed then
return
end
local tags = {}
local categories = {}
for _, item in ipairs(content) do
if item.jumpable and item.headline then
categories[item.headline:get_category():lower()] = true
for _, tag in ipairs(item.headline:get_tags()) do
tags[tag:lower()] = true
local values = {}
for _, agenda_view in ipairs(agenda_views) do
for _, line in ipairs(agenda_view:get_lines()) do
if line.headline then
values[line.headline:get_category()] = true
for _, tag in ipairs(line.headline:get_tags()) do
values[tag] = true
end
end
end
end
self.available_tags = tags
self.available_categories = categories
self.available_values = values
self.parsed = true
end

---@return string[]
function AgendaFilter:get_completion_list()
local list = vim.tbl_keys(self.available_tags)
return utils.concat(list, vim.tbl_keys(self.available_categories), true)
return vim.tbl_keys(self.available_values)
end

return AgendaFilter
Loading

0 comments on commit 8667167

Please sign in to comment.