Skip to content
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

feat(agenda)!: rewrite agenda rendering and fix filters #848

Merged
merged 1 commit into from
Jan 10, 2025
Merged
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
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
Loading