diff --git a/DOCS.md b/DOCS.md
index fcc0cddc0..989e14fdf 100644
--- a/DOCS.md
+++ b/DOCS.md
@@ -506,8 +506,18 @@ Determine on which day the week will start in calendar modal (ex: [changing the
#### **emacs_config**
_type_: `table`
-_default value_: `{ executable_path = 'emacs', config_path='$HOME/.emacs.d/init.el' }`
+_default value_: `{ executable_path = 'emacs', config_path=nil }`
Set configuration for your emacs. This is useful for having the emacs export properly pickup your emacs config and plugins.
+If `config_path` is not provided, exporter tries to find a configuration file from these locations:
+
+1. `~/.config/emacs/init.el`
+2. `~/.emacs.d/init.el`
+3. `~/.emacs.el`
+
+If there is no configuration found, it will still process the export.
+
+If it finds a configuration and export attempt fails because of the configuration issue, there will be a prompt to
+attempt the same export without the configuration file.
### Agenda settings
@@ -548,6 +558,107 @@ Example:
If `org_agenda_start_on_weekday` is `false`, and `org_agenda_start_day` is `-2d`,
agenda will always show current week from today - 2 days
+#### **org_agenda_custom_commands**
+
+_type_: `table`
+_default value_: `{}`
+
+Define custom agenda views that are available through the (org_agenda)[#org_agenda] mapping.
+It is possible to combine multiple agenda types into single view.
+An example:
+
+```lua
+require('orgmode').setup({
+ org_agenda_files = {'~/org/**/*'},
+ org_agenda_custom_commands = {
+ -- "c" is the shortcut that will be used in the prompt
+ c = {
+ description = 'Combined view', -- Description shown in the prompt for the shortcut
+ types = {
+ {
+ type = 'tags_todo', -- Type can be agenda | tags | tags_todo
+ match = '+PRIORITY="A"', --Same as providing a "Match:" for tags view oa + m, See: https://orgmode.org/manual/Matching-tags-and-properties.html
+ org_agenda_overriding_header = 'High priority todos',
+ org_agenda_todo_ignore_deadlines = 'far', -- Ignore all deadlines that are too far in future (over org_deadline_warning_days). Possible values: all | near | far | past | future
+ },
+ {
+ type = 'agenda',
+ org_agenda_overriding_header = 'My daily agenda',
+ org_agenda_span = 'day' -- can be any value as org_agenda_span
+ },
+ {
+ type = 'tags',
+ match = 'WORK', --Same as providing a "Match:" for tags view oa + m, See: https://orgmode.org/manual/Matching-tags-and-properties.html
+ org_agenda_overriding_header = 'My work todos',
+ org_agenda_todo_ignore_scheduled = 'all', -- Ignore all headlines that are scheduled. Possible values: past | future | all
+ },
+ {
+ type = 'agenda',
+ org_agenda_overriding_header = 'Whole week overview',
+ org_agenda_span = 'week', -- 'week' is default, so it's not necessary here, just an example
+ org_agenda_start_on_weekday = 1 -- Start on Monday
+ org_agenda_remove_tags = true -- Do not show tags only for this view
+ },
+ }
+ },
+ p = {
+ description = 'Personal agenda',
+ types = {
+ {
+ type = 'tags_todo',
+ org_agenda_overriding_header = 'My personal todos',
+ org_agenda_category_filter_preset = 'todos', -- Show only headlines from `todos` category. Same value providad as when pressing `/` in the Agenda view
+ org_agenda_sorting_strategy = {'todo-state-up', 'priority-down'} -- See all options available on org_agenda_sorting_strategy
+ },
+ {
+ type = 'agenda',
+ org_agenda_overriding_header = 'Personal projects agenda',
+ org_agenda_files = {'~/my-projects/**/*'}, -- Can define files outside of the default org_agenda_files
+ },
+ {
+ type = 'tags',
+ org_agenda_overriding_header = 'Personal projects notes',
+ org_agenda_files = {'~/my-projects/**/*'},
+ org_agenda_tag_filter_preset = 'NOTES-REFACTOR' -- Show only headlines with NOTES tag that does not have a REFACTOR tag. Same value providad as when pressing `/` in the Agenda view
+ },
+ }
+ }
+ }
+})
+```
+
+#### **org_agenda_sorting_strategy**
+_type_: `table<'agenda' | 'todo' | 'tags', OrgAgendaSortingStrategy[]><`
+default value: `{ agenda = {'time-up', 'priority-down', 'category-keep'}, todo = {'priority-down', 'category-keep'}, tags = {'priority-down', 'category-keep'}}`
+List of sorting strategies to apply to a given view.
+Available strategies:
+
+- `time-up` - Sort entries by time of day. Applicable only in `agenda` view
+- `time-down` - Opposite of `time-up`
+- `priority-down` - Sort by priority, from highest to lowest
+- `priority-up` - Sort by priority, from lowest to highest
+- `tag-up` - Sort by sorted tags string, ascending
+- `tag-down` - Sort by sorted tags string, descending
+- `todo-state-up` - Sort by todo keyword by position (example: 'TODO, PROGRESS, DONE' has a sort value of 1, 2 and 3), ascending
+- `todo-state-down` - Sort by todo keyword, descending
+- `clocked-up` - Show clocked in headlines first
+- `clocked-down` - Show clocked in headines last
+- `category-up` - Sort by category name, ascending
+- `category-down` - Sort by category name, descending
+- `category-keep` - Keep default category sorting, as it appears in org-agenda-files
+
+
+#### **org_agenda_block_separator**
+_type_: `string`
+default value: `-`
+Separator used to separate multiple agenda views generated by org_agenda_custom_commands.
+To change the highlight, override `@org.agenda.separator` hl group.
+
+#### **org_agenda_remove_tags**
+_type_: `boolean`
+default value: `false`
+Should tags be hidden from all agenda views.
+
#### **org_capture_templates**
_type_: `table`
diff --git a/lua/orgmode/agenda/agenda_item.lua b/lua/orgmode/agenda/agenda_item.lua
index ef07987bc..ee311e74b 100644
--- a/lua/orgmode/agenda/agenda_item.lua
+++ b/lua/orgmode/agenda/agenda_item.lua
@@ -20,6 +20,7 @@ end
---@field is_in_date_range boolean
---@field date_range_days number
---@field label string
+---@field index number
local AgendaItem = {}
---@param headline_date OrgDate single date in a headline
diff --git a/lua/orgmode/agenda/filter.lua b/lua/orgmode/agenda/filter.lua
index f8d38d746..543c0e12b 100644
--- a/lua/orgmode/agenda/filter.lua
+++ b/lua/orgmode/agenda/filter.lua
@@ -1,18 +1,22 @@
---@class OrgAgendaFilter
---@field value string
---@field available_values table
+---@field types? ('tags' | 'categories')[]
---@field values table[]
---@field term string
---@field parsed boolean
local AgendaFilter = {}
+---@param opts? { types?: ('tags' | 'categories')[] }
---@return OrgAgendaFilter
-function AgendaFilter:new()
+function AgendaFilter:new(opts)
+ opts = opts or {}
local data = {
value = '',
available_values = {},
values = {},
term = '',
+ types = opts.types or { 'tags', 'categories' },
parsed = false,
}
setmetatable(data, self)
@@ -52,13 +56,31 @@ end
---@param headline OrgHeadline
---@return boolean
function AgendaFilter:_match(headline)
+ local filters = {}
+ if vim.tbl_contains(self.types, 'tags') then
+ table.insert(filters, function(tag)
+ return headline:has_tag(tag)
+ end)
+ end
+ if vim.tbl_contains(self.types, 'categories') then
+ table.insert(filters, function(category)
+ return headline:matches_category(category)
+ end)
+ end
for _, value in ipairs(self.values) do
if value.operator == '-' then
- if headline:has_tag(value.value) or headline:matches_category(value.value) then
+ for _, filter in ipairs(filters) do
+ if filter(value.value) then
+ return false
+ end
+ end
+ else
+ local result = vim.tbl_filter(function(filter)
+ return filter(value.value)
+ end, filters)
+ if #result == 0 then
return false
end
- elseif not headline:has_tag(value.value) and not headline:matches_category(value.value) then
- return false
end
end
@@ -104,9 +126,13 @@ function AgendaFilter:parse_available_filters(agenda_views)
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
+ if vim.tbl_contains(self.types, 'categories') then
+ values[line.headline:get_category()] = true
+ end
+ if vim.tbl_contains(self.types, 'tags') then
+ for _, tag in ipairs(line.headline:get_tags()) do
+ values[tag] = true
+ end
end
end
end
diff --git a/lua/orgmode/agenda/init.lua b/lua/orgmode/agenda/init.lua
index 261a1cc5c..1c1982eea 100644
--- a/lua/orgmode/agenda/init.lua
+++ b/lua/orgmode/agenda/init.lua
@@ -13,9 +13,10 @@ local AgendaTypes = require('orgmode.agenda.types')
---@field views OrgAgendaViewType[]
---@field filters OrgAgendaFilter
---@field files OrgFiles
+---@field highlighter OrgHighlighter
local Agenda = {}
----@param opts? table
+---@param opts? { highlighter: OrgHighlighter, files: OrgFiles }
function Agenda:new(opts)
opts = opts or {}
local data = {
@@ -24,6 +25,7 @@ function Agenda:new(opts)
content = {},
highlights = {},
files = opts.files,
+ highlighter = opts.highlighter,
}
setmetatable(data, self)
self.__index = self
@@ -37,6 +39,7 @@ function Agenda:open_view(type, opts)
local view_opts = vim.tbl_extend('force', opts or {}, {
files = self.files,
agenda_filter = self.filters,
+ highlighter = self.highlighter,
})
local view = AgendaTypes[type]:new(view_opts)
@@ -53,7 +56,7 @@ function Agenda:render()
for i, view in ipairs(self.views) do
view:render(bufnr, line)
if #self.views > 1 and i < #self.views then
- colors.add_hr(bufnr, vim.fn.line('$'))
+ colors.add_hr(bufnr, vim.fn.line('$'), config.org_agenda_block_separator)
end
end
vim.bo[bufnr].modifiable = false
@@ -90,6 +93,74 @@ function Agenda:tags_todo(opts)
return self:open_view('tags_todo', opts)
end
+function Agenda:_build_custom_commands()
+ if not config.org_agenda_custom_commands then
+ return {}
+ end
+ local custom_commands = {}
+ ---@param opts OrgAgendaCustomCommandType
+ local get_type_opts = function(opts, id)
+ local opts_by_type = {
+ agenda = {
+ span = opts.org_agenda_span,
+ start_day = opts.org_agenda_start_day,
+ start_on_weekday = opts.org_agenda_start_on_weekday,
+ },
+ tags = {
+ match_query = opts.match,
+ todo_ignore_scheduled = opts.org_agenda_todo_ignore_scheduled,
+ todo_ignore_deadlines = opts.org_agenda_todo_ignore_deadlines,
+ },
+ tags_todo = {
+ match_query = opts.match,
+ todo_ignore_scheduled = opts.org_agenda_todo_ignore_scheduled,
+ todo_ignore_deadlines = opts.org_agenda_todo_ignore_deadlines,
+ },
+ }
+
+ if not opts_by_type[opts.type] then
+ return
+ end
+
+ opts_by_type[opts.type].sorting_strategy = opts.org_agenda_sorting_strategy
+ opts_by_type[opts.type].filters = self.filters
+ opts_by_type[opts.type].files = self.files
+ opts_by_type[opts.type].header = opts.org_agenda_overriding_header
+ opts_by_type[opts.type].agenda_files = opts.org_agenda_files
+ opts_by_type[opts.type].tag_filter = opts.org_agenda_tag_filter_preset
+ opts_by_type[opts.type].category_filter = opts.org_agenda_category_filter_preset
+ opts_by_type[opts.type].highlighter = self.highlighter
+ opts_by_type[opts.type].remove_tags = opts.org_agenda_remove_tags
+ opts_by_type[opts.type].id = id
+
+ return opts_by_type[opts.type]
+ end
+ for shortcut, command in pairs(config.org_agenda_custom_commands) do
+ table.insert(custom_commands, {
+ label = command.description or '',
+ key = shortcut,
+ action = function()
+ local views = {}
+ for i, agenda_type in ipairs(command.types) do
+ local opts = get_type_opts(agenda_type, ('%s_%s_%d'):format(shortcut, agenda_type.type, i))
+ if not opts then
+ utils.echo_error('Invalid custom agenda command type ' .. agenda_type.type)
+ break
+ end
+ table.insert(views, AgendaTypes[agenda_type.type]:new(opts))
+ end
+ self.views = views
+ local result = self:render()
+ if #self.views > 1 then
+ vim.fn.cursor({ 1, 0 })
+ end
+ return result
+ end,
+ })
+ end
+ return custom_commands
+end
+
---@private
---@return number buffer number
function Agenda:_open_window()
@@ -157,6 +228,18 @@ function Agenda:prompt()
return self:search()
end,
})
+
+ local custom_commands = self:_build_custom_commands()
+ if #custom_commands > 0 then
+ for _, command in ipairs(custom_commands) do
+ menu:add_option({
+ label = command.label,
+ key = command.key,
+ action = command.action,
+ })
+ end
+ end
+
menu:add_option({ label = 'Quit', key = 'q' })
menu:add_separator({ icon = ' ', length = 1 })
@@ -169,10 +252,11 @@ end
---@param source? string
function Agenda:redo(source, preserve_cursor_pos)
+ self:_call_all_views('redo')
return self.files:load(true):next(vim.schedule_wrap(function()
local save_view = preserve_cursor_pos and vim.fn.winsaveview()
if source == 'mapping' then
- self:_call_view_and_render('redo')
+ self:_call_view_and_render('redraw')
end
self:render()
if save_view then
@@ -478,6 +562,18 @@ function Agenda:_call_view(method, ...)
return executed
end
+function Agenda:_call_all_views(method, ...)
+ local executed = false
+ for _, view in ipairs(self.views) do
+ if view[method] then
+ view[method](view, ...)
+ executed = true
+ end
+ end
+
+ return executed
+end
+
function Agenda:_call_view_and_render(method, ...)
local executed = self:_call_view(method, ...)
if executed then
diff --git a/lua/orgmode/agenda/sorting_strategy.lua b/lua/orgmode/agenda/sorting_strategy.lua
new file mode 100644
index 000000000..3dc19b216
--- /dev/null
+++ b/lua/orgmode/agenda/sorting_strategy.lua
@@ -0,0 +1,218 @@
+local utils = require('orgmode.utils')
+---@alias OrgAgendaSortingStrategy
+---| 'time-up'
+---| 'time-down'
+---| 'priority-down'
+---| 'priority-up'
+---| 'tag-up'
+---| 'tag-down'
+---| 'todo-state-up'
+---| 'todo-state-down'
+---| 'clocked-up'
+---| 'clocked-down'
+---| 'category-up'
+---| 'category-down'
+---| 'category-keep'
+local SortingStrategy = {}
+
+---@class SortableEntry
+---@field date OrgDate Available only in agenda view
+---@field headline OrgHeadline
+---@field index number Index of the entry in the fetched list
+---@field is_day_match? boolean Is this entry a match for the given day. Available only in agenda view
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.time_up(a, b)
+ if a.is_day_match and b.is_day_match then
+ if a.date:has_time() and b.date:has_time() then
+ if a.date.timestamp ~= b.date.timestamp then
+ return a.date.timestamp < b.date.timestamp
+ end
+ return
+ end
+ end
+ if a.is_day_match and a.date:has_time() then
+ return true
+ end
+
+ if b.is_day_match and b.date:has_time() then
+ return false
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.time_down(a, b)
+ local time_up = SortingStrategy.time_up(a, b)
+ if type(time_up) == 'boolean' then
+ return not time_up
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.priority_down(a, b)
+ if a.headline:get_priority_sort_value() ~= b.headline:get_priority_sort_value() then
+ return a.headline:get_priority_sort_value() > b.headline:get_priority_sort_value()
+ end
+ if a.date and b.date then
+ local is_same = a.date:is_same(b.date)
+ if not is_same then
+ return a.date:is_before(b.date)
+ end
+ if a.date.type ~= b.date.type then
+ return a.date:get_type_sort_value() < b.date:get_type_sort_value()
+ end
+ end
+end
+
+function SortingStrategy.priority_up(a, b)
+ local priority_down = SortingStrategy.priority_down(a, b)
+ if type(priority_down) == 'boolean' then
+ return not priority_down
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.tag_up(a, b)
+ local a_tags = a.headline:tags_to_string(true)
+ local b_tags = b.headline:tags_to_string(true)
+ if a_tags == '' and b_tags == '' then
+ return
+ end
+ if a_tags == b_tags then
+ return
+ end
+ if a_tags == '' then
+ return false
+ end
+ if b_tags == '' then
+ return true
+ end
+ return a_tags < b_tags
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.tag_down(a, b)
+ local tag_up = SortingStrategy.tag_up(a, b)
+ if type(tag_up) == 'boolean' then
+ return not tag_up
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.todo_state_up(a, b)
+ local _, _, _, a_index = a.headline:get_todo()
+ local _, _, _, b_index = b.headline:get_todo()
+ if a_index and b_index then
+ if a_index ~= b_index then
+ return a_index < b_index
+ end
+ return nil
+ end
+ if a_index then
+ return true
+ end
+ if b_index then
+ return false
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.todo_state_down(a, b)
+ local todo_state_up = SortingStrategy.todo_state_up(a, b)
+ if type(todo_state_up) == 'boolean' then
+ return not todo_state_up
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.category_up(a, b)
+ if a.headline.file:get_category() ~= b.headline.file:get_category() then
+ return a.headline.file:get_category() < b.headline.file:get_category()
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.category_down(a, b)
+ local category_up = SortingStrategy.category_up(a, b)
+ if type(category_up) == 'boolean' then
+ return not category_up
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.category_keep(a, b)
+ if a.headline.file.index ~= b.headline.file.index then
+ return a.headline.file.index < b.headline.file.index
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.clocked_up(a, b)
+ if a.headline:is_clocked_in() and not b.headline:is_clocked_in() then
+ return true
+ end
+ if not a.headline:is_clocked_in() and b.headline:is_clocked_in() then
+ return false
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+function SortingStrategy.clocked_down(a, b)
+ local clocked_up = SortingStrategy.clocked_up(a, b)
+ if type(clocked_up) == 'boolean' then
+ return not clocked_up
+ end
+end
+
+---@param a SortableEntry
+---@param b SortableEntry
+local fallback_sort = function(a, b)
+ if a.headline.file.index ~= b.headline.file.index then
+ return a.headline.file.index < b.headline.file.index
+ end
+
+ return a.index < b.index
+end
+
+---@generic T
+---@param items T[]
+---@param strategies OrgAgendaSortingStrategy[]
+---@param make_entry fun(item: T): SortableEntry
+local function sort(items, strategies, make_entry)
+ table.sort(items, function(a, b)
+ local entry_a = make_entry(a)
+ local entry_b = make_entry(b)
+
+ for _, fn in ipairs(strategies) do
+ local sorting_fn = SortingStrategy[fn:gsub('-', '_')]
+ if not sorting_fn then
+ utils.echo_error('Unknown sorting strategy: ' .. fn)
+ break
+ end
+ local result = sorting_fn(entry_a, entry_b)
+ if result ~= nil then
+ return result
+ end
+ end
+
+ return fallback_sort(entry_a, entry_b)
+ end)
+
+ return items
+end
+
+return {
+ sort = sort,
+}
diff --git a/lua/orgmode/agenda/types/agenda.lua b/lua/orgmode/agenda/types/agenda.lua
index 6000154fa..509ea8bdf 100644
--- a/lua/orgmode/agenda/types/agenda.lua
+++ b/lua/orgmode/agenda/types/agenda.lua
@@ -1,4 +1,5 @@
local Date = require('orgmode.objects.date')
+local Files = require('orgmode.files')
local config = require('orgmode.config')
local AgendaFilter = require('orgmode.agenda.filter')
local AgendaItem = require('orgmode.agenda.agenda_item')
@@ -7,6 +8,7 @@ local AgendaLine = require('orgmode.agenda.view.line')
local AgendaLineToken = require('orgmode.agenda.view.token')
local ClockReport = require('orgmode.clock.report')
local utils = require('orgmode.utils')
+local SortingStrategy = require('orgmode.agenda.sorting_strategy')
---@class OrgAgendaViewType
---@field render fun(self: OrgAgendaViewType, bufnr:number, current_line?: number): OrgAgendaView
@@ -17,20 +19,30 @@ local utils = require('orgmode.utils')
---@class OrgAgendaTypeOpts
---@field files OrgFiles
+---@field highlighter OrgHighlighter
---@field agenda_filter OrgAgendaFilter
---@field filter? string
+---@field tag_filter? string
+---@field category_filter? string
+---@field agenda_files string | string[] | nil
---@field span? OrgAgendaSpan
---@field from? OrgDate
---@field start_on_weekday? number
---@field start_day? string
---@field header? string
---@field show_clock_report? boolean
----@field is_custom? boolean
+---@field sorting_strategy? OrgAgendaSortingStrategy[]
+---@field remove_tags? boolean
+---@field id? string
---@class OrgAgendaType:OrgAgendaViewType
---@field files OrgFiles
+---@field highlighter OrgHighlighter
---@field agenda_filter OrgAgendaFilter
---@field filter? OrgAgendaFilter
+---@field tag_filter? OrgAgendaFilter
+---@field category_filter? OrgAgendaFilter
+---@field agenda_files string | string[] | nil
---@field span? OrgAgendaSpan
---@field from? OrgDate
---@field to? OrgDate
@@ -41,7 +53,9 @@ local utils = require('orgmode.utils')
---@field show_clock_report? boolean
---@field clock_report? OrgClockReport
---@field clock_report_view? OrgAgendaView
----@field is_custom? boolean
+---@field sorting_strategy? OrgAgendaSortingStrategy[]
+---@field remove_tags? boolean
+---@field id? string
local OrgAgendaType = {}
OrgAgendaType.__index = OrgAgendaType
@@ -49,8 +63,12 @@ OrgAgendaType.__index = OrgAgendaType
function OrgAgendaType:new(opts)
local data = {
files = opts.files,
+ highlighter = opts.highlighter,
agenda_filter = opts.agenda_filter,
filter = opts.filter and AgendaFilter:new():parse(opts.filter, true) or nil,
+ tag_filter = opts.tag_filter and AgendaFilter:new({ types = { 'tags' } }):parse(opts.tag_filter, true) or nil,
+ category_filter = opts.category_filter and AgendaFilter:new({ types = { 'categories' } })
+ :parse(opts.category_filter, true) or nil,
span = opts.span or config:get_agenda_span(),
from = opts.from or Date.now():start_of('day'),
to = nil,
@@ -58,14 +76,34 @@ function OrgAgendaType:new(opts)
show_clock_report = opts.show_clock_report or false,
start_on_weekday = opts.start_on_weekday or config.org_agenda_start_on_weekday,
start_day = opts.start_day or config.org_agenda_start_day,
+ agenda_files = opts.agenda_files,
header = opts.header,
- is_custom = opts.is_custom or false,
+ sorting_strategy = opts.sorting_strategy or vim.tbl_get(config.org_agenda_sorting_strategy, 'agenda') or {},
+ id = opts.id,
+ remove_tags = type(opts.remove_tags) == 'boolean' and opts.remove_tags or config.org_agenda_remove_tags,
}
local this = setmetatable(data, OrgAgendaType)
this:_set_date_range()
+ this:_setup_agenda_files()
return this
end
+function OrgAgendaType:redo()
+ if self.agenda_files then
+ self.files:load_sync(true)
+ end
+end
+
+function OrgAgendaType:_setup_agenda_files()
+ if not self.agenda_files then
+ return
+ end
+ self.files = Files:new({
+ paths = self.agenda_files,
+ cache = true,
+ }):load_sync(true)
+end
+
function OrgAgendaType:advance_span(count, direction)
count = count or 1
direction = direction * count
@@ -188,7 +226,7 @@ function OrgAgendaType:render(bufnr, current_line)
end
local agenda_days = self:_get_agenda_days()
- local agendaView = AgendaView:new({ bufnr = self.bufnr })
+ local agendaView = AgendaView:new({ bufnr = self.bufnr, highlighter = self.highlighter })
agendaView:add_line(AgendaLine:single_token({
content = self:_get_title(),
hl_group = '@org.agenda.header',
@@ -304,8 +342,9 @@ function OrgAgendaType:_build_line(agenda_item, metadata)
end
line:add_token(AgendaLineToken:new({
content = headline:get_title(),
+ add_markup_to_headline = headline,
}))
- if #headline:get_tags() > 0 then
+ if not self.remove_tags and #headline:get_tags() > 0 then
local tags_string = headline:tags_to_string()
line:add_token(AgendaLineToken:new({
content = tags_string,
@@ -349,11 +388,7 @@ function OrgAgendaType:_get_agenda_days()
for index, item in ipairs(headline_dates) do
local headline = item.headline
local agenda_item = AgendaItem:new(item.headline_date, headline, day, index)
- if
- agenda_item.is_valid
- and self.agenda_filter:matches(headline)
- and (not self.filter or self.filter:matches(headline))
- then
+ if agenda_item.is_valid and self:_matches_filters(headline) then
table.insert(headlines, headline)
table.insert(date.agenda_items, agenda_item)
date.category_length = math.max(date.category_length, vim.api.nvim_strwidth(headline:get_category()))
@@ -361,7 +396,7 @@ function OrgAgendaType:_get_agenda_days()
end
end
- date.agenda_items = self._sort(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)
@@ -376,6 +411,22 @@ function OrgAgendaType:toggle_clock_report()
return self
end
+function OrgAgendaType:_matches_filters(headline)
+ local valid_filters = {
+ self.filter,
+ self.tag_filter,
+ self.category_filter,
+ self.agenda_filter,
+ }
+
+ for _, filter in pairs(valid_filters) do
+ if filter and not filter:matches(headline) then
+ return false
+ end
+ end
+ return true
+end
+
function OrgAgendaType:_set_date_range(from)
local span = self.span
from = from or self.from
@@ -422,49 +473,21 @@ function OrgAgendaType:_format_day(day)
return string.format('%-10s %s', day:format('%A'), day:format('%d %B %Y'))
end
-local function sort_by_date_or_priority_or_category(a, b)
- if a.headline:get_priority_sort_value() ~= b.headline:get_priority_sort_value() then
- return a.headline:get_priority_sort_value() > b.headline:get_priority_sort_value()
- end
- if not a.real_date:is_same(b.real_date, 'day') then
- return a.real_date:is_before(b.real_date)
- end
- return a.index < b.index
-end
-
---@private
---@param agenda_items OrgAgendaItem[]
---@return OrgAgendaItem[]
-function OrgAgendaType._sort(agenda_items)
- table.sort(agenda_items, function(a, b)
- if a.is_same_day and b.is_same_day then
- if a.real_date:has_time() and not b.real_date:has_time() then
- return true
- end
- if b.real_date:has_time() and not a.real_date:has_time() then
- return false
- end
- if a.real_date:has_time() and b.real_date:has_time() then
- return a.real_date:is_before(b.real_date)
- end
- return sort_by_date_or_priority_or_category(a, b)
- end
-
- if a.is_same_day and not b.is_same_day then
- if a.real_date:has_time() or (b.real_date:is_none() and not a.real_date:is_none()) then
- return true
- end
- end
-
- if not a.is_same_day and b.is_same_day then
- if b.real_date:has_time() or (a.real_date:is_none() and not b.real_date:is_none()) then
- return false
- end
- end
+function OrgAgendaType:_sort(agenda_items)
+ ---@param agenda_item OrgAgendaItem
+ local make_entry = function(agenda_item)
+ return {
+ date = agenda_item.real_date,
+ headline = agenda_item.headline,
+ index = agenda_item.index,
+ is_day_match = agenda_item.is_same_day,
+ }
+ end
- return sort_by_date_or_priority_or_category(a, b)
- end)
- return agenda_items
+ return SortingStrategy.sort(agenda_items, self.sorting_strategy, make_entry)
end
return OrgAgendaType
diff --git a/lua/orgmode/agenda/types/search.lua b/lua/orgmode/agenda/types/search.lua
index c94657a99..5bd24d1e5 100644
--- a/lua/orgmode/agenda/types/search.lua
+++ b/lua/orgmode/agenda/types/search.lua
@@ -31,9 +31,9 @@ function OrgAgendaSearchType:get_search_term()
return vim.fn.OrgmodeInput('Enter search term: ', self.headline_query or '')
end
-function OrgAgendaSearchType:redo()
+function OrgAgendaSearchType:redraw()
-- Skip prompt for custom views
- if self.is_custom then
+ if self.id then
return self
end
self.headline_query = self:get_search_term()
diff --git a/lua/orgmode/agenda/types/tags.lua b/lua/orgmode/agenda/types/tags.lua
index b74049855..6901624d5 100644
--- a/lua/orgmode/agenda/types/tags.lua
+++ b/lua/orgmode/agenda/types/tags.lua
@@ -1,21 +1,35 @@
---@diagnostic disable: inject-field
+local Date = require('orgmode.objects.date')
+local config = require('orgmode.config')
local utils = require('orgmode.utils')
+local validator = require('orgmode.utils.validator')
local Search = require('orgmode.files.elements.search')
local OrgAgendaTodosType = require('orgmode.agenda.types.todo')
+---@alias OrgAgendaTodoIgnoreDeadlinesTypes 'all' | 'near' | 'far' | 'past' | 'future'
+---@alias OrgAgendaTodoIgnoreScheduledTypes 'all' | 'past' | 'future'
+
---@class OrgAgendaTagsTypeOpts:OrgAgendaTodosTypeOpts
---@field match_query? string
+---@field todo_ignore_deadlines OrgAgendaTodoIgnoreDeadlinesTypes
+---@field todo_ignore_scheduled OrgAgendaTodoIgnoreScheduledTypes
---@class OrgAgendaTagsType:OrgAgendaTodosType
+---@field match_query string
+---@field todo_ignore_deadlines OrgAgendaTodoIgnoreDeadlinesTypes
+---@field todo_ignore_scheduled OrgAgendaTodoIgnoreScheduledTypes
local OrgAgendaTagsType = {}
OrgAgendaTagsType.__index = OrgAgendaTagsType
---@param opts OrgAgendaTagsTypeOpts
function OrgAgendaTagsType:new(opts)
opts.todo_only = opts.todo_only or false
- opts.subheader = 'Press "r" to update search'
+ opts.sorting_strategy = opts.sorting_strategy or vim.tbl_get(config.org_agenda_sorting_strategy, 'tags') or {}
+ if not opts.id then
+ opts.subheader = 'Press "r" to update search'
+ end
local match_query = opts.match_query
- if not match_query or match_query == '' then
+ if not opts.id and (not match_query or match_query == '') then
match_query = self:get_tags(opts.files)
if not match_query then
return nil
@@ -25,13 +39,60 @@ function OrgAgendaTagsType:new(opts)
setmetatable(self, { __index = OrgAgendaTodosType })
local obj = OrgAgendaTodosType:new(opts)
setmetatable(obj, self)
- obj.match_query = match_query
+ obj.match_query = match_query or ''
+ obj.todo_ignore_deadlines = opts.todo_ignore_deadlines
+ obj.todo_ignore_scheduled = opts.todo_ignore_scheduled
obj.header = 'Headlines with TAGS match: ' .. obj.match_query
return obj
end
function OrgAgendaTagsType:get_file_headlines(file)
- return file:apply_search(Search:new(self.match_query or ''), self.todo_only)
+ local headlines = file:apply_search(Search:new(self.match_query), self.todo_only)
+ if self.todo_ignore_deadlines then
+ headlines = vim.tbl_filter(function(headline) ---@cast headline OrgHeadline
+ local deadline_date = headline:get_deadline_date()
+ if not deadline_date then
+ return true
+ end
+ if self.todo_ignore_deadlines == 'all' then
+ return false
+ end
+ if self.todo_ignore_deadlines == 'near' then
+ local diff = deadline_date:diff(Date.now())
+ return diff > config.org_deadline_warning_days
+ end
+ if self.todo_ignore_deadlines == 'far' then
+ local diff = deadline_date:diff(Date.now())
+ return diff <= config.org_deadline_warning_days
+ end
+ if self.todo_ignore_deadlines == 'past' then
+ return not deadline_date:is_same_or_before(Date.today(), 'day')
+ end
+ if self.todo_ignore_deadlines == 'future' then
+ return not deadline_date:is_after(Date.today(), 'day')
+ end
+ return true
+ end, headlines)
+ end
+ if self.todo_ignore_scheduled then
+ headlines = vim.tbl_filter(function(headline) ---@cast headline OrgHeadline
+ local scheduled_date = headline:get_scheduled_date()
+ if not scheduled_date then
+ return true
+ end
+ if self.todo_ignore_scheduled == 'all' then
+ return false
+ end
+ if self.todo_ignore_scheduled == 'past' then
+ return scheduled_date:is_same_or_before(Date.today(), 'day')
+ end
+ if self.todo_ignore_scheduled == 'future' then
+ return scheduled_date:is_after(Date.today(), 'day')
+ end
+ return true
+ end, headlines)
+ end
+ return headlines
end
---@param files? OrgFiles
@@ -45,9 +106,9 @@ function OrgAgendaTagsType:get_tags(files)
return tags
end
-function OrgAgendaTagsType:redo()
+function OrgAgendaTagsType:redraw()
-- Skip prompt for custom views
- if self.is_custom then
+ if self.id then
return self
end
self.match_query = self:get_tags() or ''
diff --git a/lua/orgmode/agenda/types/todo.lua b/lua/orgmode/agenda/types/todo.lua
index 707a53e21..03c6d45cc 100644
--- a/lua/orgmode/agenda/types/todo.lua
+++ b/lua/orgmode/agenda/types/todo.lua
@@ -1,59 +1,102 @@
+local config = require('orgmode.config')
local AgendaView = require('orgmode.agenda.view.init')
+local Files = require('orgmode.files')
local AgendaLine = require('orgmode.agenda.view.line')
local AgendaFilter = require('orgmode.agenda.filter')
local AgendaLineToken = require('orgmode.agenda.view.token')
local utils = require('orgmode.utils')
local agenda_highlights = require('orgmode.colors.highlights')
local hl_map = agenda_highlights.get_agenda_hl_map()
+local SortingStrategy = require('orgmode.agenda.sorting_strategy')
---@class OrgAgendaTodosTypeOpts
---@field files OrgFiles
+---@field highlighter OrgHighlighter
---@field agenda_filter OrgAgendaFilter
---@field filter? string
+---@field tag_filter? string
+---@field category_filter? string
+---@field agenda_files string | string[] | nil
---@field header? string
---@field subheader? string
---@field todo_only? boolean
----@field is_custom? boolean
+---@field sorting_strategy? OrgAgendaSortingStrategy[]
+---@field remove_tags? boolean
+---@field id? string
---@class OrgAgendaTodosType:OrgAgendaViewType
---@field files OrgFiles
+---@field highlighter OrgHighlighter
---@field agenda_filter OrgAgendaFilter
---@field filter? OrgAgendaFilter
+---@field tag_filter? string
+---@field category_filter? string
+---@field agenda_files string | string[] | nil
---@field header? string
---@field subheader? string
---@field bufnr? number
---@field todo_only? boolean
----@field is_custom? boolean
+---@field sorting_strategy? OrgAgendaSortingStrategy[]
+---@field remove_tags? boolean
+---@field id? string
local OrgAgendaTodosType = {}
OrgAgendaTodosType.__index = OrgAgendaTodosType
---@param opts OrgAgendaTodosTypeOpts
function OrgAgendaTodosType:new(opts)
- return setmetatable({
+ local this = setmetatable({
files = opts.files,
+ highlighter = opts.highlighter,
agenda_filter = opts.agenda_filter,
filter = opts.filter and AgendaFilter:new():parse(opts.filter, true) or nil,
+ tag_filter = opts.tag_filter and AgendaFilter:new({ types = { 'tags' } }):parse(opts.tag_filter, true) or nil,
+ category_filter = opts.category_filter and AgendaFilter:new({ types = { 'categories' } })
+ :parse(opts.category_filter, true) or nil,
header = opts.header,
subheader = opts.subheader,
+ agenda_files = opts.agenda_files,
todo_only = opts.todo_only == nil and true or opts.todo_only,
- is_custom = opts.is_custom or false,
+ sorting_strategy = opts.sorting_strategy or vim.tbl_get(config.org_agenda_sorting_strategy, 'todo') or {},
+ id = opts.id,
+ remove_tags = type(opts.remove_tags) == 'boolean' and opts.remove_tags or config.org_agenda_remove_tags,
}, OrgAgendaTodosType)
+
+ this:_setup_agenda_files()
+ return this
+end
+
+function OrgAgendaTodosType:_setup_agenda_files()
+ if not self.agenda_files then
+ return
+ end
+ self.files = Files:new({
+ paths = self.agenda_files,
+ cache = true,
+ }):load_sync(true)
+end
+
+function OrgAgendaTodosType:redo()
+ if self.agenda_files then
+ self.files:load_sync(true)
+ end
end
---@param bufnr? number
function OrgAgendaTodosType:render(bufnr)
self.bufnr = bufnr or 0
local headlines, category_length = self:_get_headlines()
- local agendaView = AgendaView:new({ bufnr = self.bufnr })
+ local agendaView = AgendaView:new({ bufnr = self.bufnr, highlighter = self.highlighter })
agendaView:add_line(AgendaLine:single_token({
content = self.header or 'Global list of TODO items of type: ALL',
hl_group = '@org.agenda.header',
}))
- agendaView:add_line(AgendaLine:single_token({
- content = self.subheader or '',
- hl_group = '@org.agenda.header',
- }))
+ if self.subheader then
+ agendaView:add_line(AgendaLine:single_token({
+ content = self.subheader,
+ hl_group = '@org.agenda.header',
+ }))
+ end
for _, headline in ipairs(headlines) do
agendaView:add_line(self:_build_line(headline, { category_length = category_length }))
@@ -94,8 +137,9 @@ function OrgAgendaTodosType:_build_line(headline, metadata)
end
line:add_token(AgendaLineToken:new({
content = headline:get_title(),
+ add_markup_to_headline = headline,
}))
- if #headline:get_tags() > 0 then
+ if not self.remove_tags and #headline:get_tags() > 0 then
local tags_string = headline:tags_to_string()
line:add_token(AgendaLineToken:new({
content = tags_string,
@@ -143,9 +187,11 @@ function OrgAgendaTodosType:_get_headlines()
for _, orgfile in ipairs(self.files:all()) do
local headlines = self:get_file_headlines(orgfile)
- for _, headline in ipairs(headlines) do
- if self.agenda_filter:matches(headline) and (not self.filter or self.filter:matches(headline)) then
+ for i, headline in ipairs(headlines) do
+ if self:_matches_filters(headline) then
category_length = math.max(category_length, vim.api.nvim_strwidth(headline:get_category()))
+ ---@diagnostic disable-next-line: inject-field
+ headline.index = i
table.insert(items, headline)
end
end
@@ -155,17 +201,35 @@ function OrgAgendaTodosType:_get_headlines()
return items, category_length + 1
end
+function OrgAgendaTodosType:_matches_filters(headline)
+ local valid_filters = {
+ self.agenda_filter,
+ self.filter,
+ self.tag_filter,
+ self.category_filter,
+ }
+
+ for _, filter in ipairs(valid_filters) do
+ if filter and not filter:matches(headline) then
+ return false
+ end
+ end
+ return true
+end
+
---@private
---@param todos OrgHeadline[]
---@return OrgHeadline[]
function OrgAgendaTodosType:_sort(todos)
- table.sort(todos, function(a, b)
- if a:get_priority_sort_value() ~= b:get_priority_sort_value() then
- return a:get_priority_sort_value() > b:get_priority_sort_value()
- end
- return a:get_category() < b:get_category()
- end)
- return todos
+ ---@param headline OrgHeadline
+ local make_entry = function(headline)
+ return {
+ headline = headline,
+ index = headline.index,
+ is_day_match = false,
+ }
+ end
+ return SortingStrategy.sort(todos, self.sorting_strategy, make_entry)
end
return OrgAgendaTodosType
diff --git a/lua/orgmode/agenda/view/init.lua b/lua/orgmode/agenda/view/init.lua
index 24ca4354b..492ba4613 100644
--- a/lua/orgmode/agenda/view/init.lua
+++ b/lua/orgmode/agenda/view/init.lua
@@ -2,13 +2,14 @@ local colors = require('orgmode.colors')
---@class OrgAgendaView
---@field bufnr number
+---@field highlighter OrgHighlighter
---@field start_line number
---@field line_counter number
---@field lines OrgAgendaLine[]
local OrgAgendaView = {}
OrgAgendaView.__index = OrgAgendaView
----@param opts { bufnr: number }
+---@param opts { bufnr: number, highlighter: OrgHighlighter }
---@return OrgAgendaView
function OrgAgendaView:new(opts)
local line_nr = vim.api.nvim_buf_line_count(opts.bufnr)
@@ -19,6 +20,7 @@ function OrgAgendaView:new(opts)
end
return setmetatable({
bufnr = opts.bufnr,
+ highlighter = opts.highlighter,
start_line = line_nr,
end_line = line_nr,
lines = {},
@@ -29,6 +31,7 @@ end
function OrgAgendaView:add_line(line)
line.line_nr = self.end_line
line.view = self
+ line.highlighter = self.highlighter
table.insert(self.lines, line)
self.end_line = self.end_line + 1
end
@@ -45,6 +48,7 @@ end
function OrgAgendaView:replace_line(old_line, new_line)
new_line.line_nr = old_line.line_nr
new_line.view = self
+ new_line.highlighter = self.highlighter
for i, line in ipairs(self.lines) do
if line.line_nr == old_line.line_nr then
self.lines[i] = new_line
diff --git a/lua/orgmode/agenda/view/line.lua b/lua/orgmode/agenda/view/line.lua
index 041c06b13..a963648e1 100644
--- a/lua/orgmode/agenda/view/line.lua
+++ b/lua/orgmode/agenda/view/line.lua
@@ -1,8 +1,10 @@
local colors = require('orgmode.colors')
local Range = require('orgmode.files.elements.range')
local OrgAgendaLineToken = require('orgmode.agenda.view.token')
+local utils = require('orgmode.utils')
---@class OrgAgendaLineOpts
---@field headline? OrgHeadline
+---@field highlighter? OrgHighlighter
---@field hl_group? string Highlight group for the whole line content
---@field line_hl_group? string Highlight group for the whole line (including white space)
---@field metadata? table
@@ -10,6 +12,7 @@ local OrgAgendaLineToken = require('orgmode.agenda.view.token')
---@class OrgAgendaLine:OrgAgendaLineOpts
---@field view OrgAgendaView
+---@field highlighter OrgHighlighter
---@field line_nr number
---@field col_counter number
---@field headline? OrgHeadline
@@ -25,6 +28,7 @@ function OrgAgendaLine:new(opts)
tokens = {},
col_counter = 1,
headline = opts.headline,
+ highlighter = opts.highlighter,
hl_group = opts.hl_group,
line_hl_group = opts.line_hl_group,
separator = opts.separator or ' ',
@@ -44,6 +48,7 @@ end
---@param token OrgAgendaLineToken
function OrgAgendaLine:add_token(token)
-- Add offset because of the concatenation later
+ token.highlighter = self.highlighter
local concat_offset = #self.tokens > 0 and #self.separator or 0
local length = #token.content
local start_col = self.col_counter + concat_offset
@@ -86,6 +91,7 @@ function OrgAgendaLine:compile()
for _, token in ipairs(self.tokens) do
token.range.start_line = self.line_nr
token.range.end_line = self.line_nr
+ token.highlighter = self.highlighter
if token.virt_text_pos then
local hl_groups = { token.hl_group }
if self.hl_group then
@@ -100,8 +106,8 @@ function OrgAgendaLine:compile()
else
table.insert(result.content, token.content)
local hl = token:get_highlights()
- if hl then
- table.insert(result.highlights, hl)
+ if #hl > 0 then
+ vim.list_extend(result.highlights, hl)
end
end
end
diff --git a/lua/orgmode/agenda/view/token.lua b/lua/orgmode/agenda/view/token.lua
index 1af10b6c2..c90015582 100644
--- a/lua/orgmode/agenda/view/token.lua
+++ b/lua/orgmode/agenda/view/token.lua
@@ -1,9 +1,12 @@
+local Range = require('orgmode.files.elements.range')
---@class OrgAgendaLineTokenOpts
---@field content string
+---@field highlighter? OrgHighlighter
---@field range? OrgRange
---@field virt_text_pos? string
---@field hl_group? string
---@field trim_for_hl? boolean
+---@field add_markup_to_headline? OrgHeadline
---@class OrgAgendaLineToken: OrgAgendaLineTokenOpts
local OrgAgendaLineToken = {}
@@ -15,30 +18,55 @@ function OrgAgendaLineToken:new(opts)
local data = {
content = opts.content,
range = opts.range,
+ highlighter = opts.highlighter,
hl_group = opts.hl_group,
virt_text_pos = opts.virt_text_pos,
trim_for_hl = opts.trim_for_hl,
+ add_markup_to_headline = opts.add_markup_to_headline,
}
return setmetatable(data, OrgAgendaLineToken)
end
function OrgAgendaLineToken:get_highlights()
- if not self.hl_group or self.virt_text_pos then
- return nil
- end
- local range = self.range
- if self.trim_for_hl then
- range = self.range:clone()
- local start_offset = self.content:match('^%s*')
- local end_offset = self.content:match('%s*$')
- range.start_col = range.start_col + (start_offset and #start_offset or 0)
- range.end_col = range.end_col - (end_offset and #end_offset or 0)
+ local highlights = {}
+ if self.hl_group and not self.virt_text_pos then
+ local range = self.range
+ if self.trim_for_hl then
+ range = self.range:clone()
+ local start_offset = self.content:match('^%s*')
+ local end_offset = self.content:match('%s*$')
+ range.start_col = range.start_col + (start_offset and #start_offset or 0)
+ range.end_col = range.end_col - (end_offset and #end_offset or 0)
+ end
+
+ table.insert(highlights, {
+ hlgroup = self.hl_group,
+ range = range,
+ })
end
- return {
- hlgroup = self.hl_group,
- range = range,
- }
+ if self.add_markup_to_headline and self.highlighter then
+ local markup_highlights = self.highlighter.markup:get_prepared_headline_highlights(self.add_markup_to_headline)
+ local _, offset = self.add_markup_to_headline:get_title()
+
+ for _, hl in ipairs(markup_highlights) do
+ table.insert(highlights, {
+ hlgroup = hl.hl_group,
+ extmark = true,
+ range = Range:new({
+ start_line = self.range.start_line,
+ end_line = self.range.end_line,
+ start_col = self.range.start_col + hl.start_col - offset,
+ end_col = self.range.start_col + hl.end_col - offset,
+ }),
+ priority = hl.priority,
+ conceal = hl.conceal,
+ spell = hl.spell,
+ url = hl.url,
+ })
+ end
+ end
+ return highlights
end
return OrgAgendaLineToken
diff --git a/lua/orgmode/clock/report.lua b/lua/orgmode/clock/report.lua
index a1a4ac046..e77aaa22b 100644
--- a/lua/orgmode/clock/report.lua
+++ b/lua/orgmode/clock/report.lua
@@ -68,6 +68,8 @@ function ClockReport:generate_report()
}
end
+---@private
+---@param orgfile OrgFile
function ClockReport:_get_clock_report_for_file(orgfile)
local total_duration = 0
local headlines = {}
diff --git a/lua/orgmode/colors/highlighter/init.lua b/lua/orgmode/colors/highlighter/init.lua
index 4e7442863..3ab76aa1b 100644
--- a/lua/orgmode/colors/highlighter/init.lua
+++ b/lua/orgmode/colors/highlighter/init.lua
@@ -1,11 +1,11 @@
---@class OrgHighlighter
---@field namespace number
+---@field markup OrgMarkupHighlighter
---@field private stars OrgStarsHighlighter
----@field private markup OrgMarkupHighlighter
---@field private todos OrgTodosHighlighter
---@field private foldtext OrgFoldtextHighlighter
---@field private _ephemeral boolean
----@field private buffers table
+---@field private buffers table
local OrgHighlighter = {}
local config = require('orgmode.config')
diff --git a/lua/orgmode/colors/highlighter/markup/_meta.lua b/lua/orgmode/colors/highlighter/markup/_meta.lua
index 14742f5fb..a8ab3dd9b 100644
--- a/lua/orgmode/colors/highlighter/markup/_meta.lua
+++ b/lua/orgmode/colors/highlighter/markup/_meta.lua
@@ -18,8 +18,20 @@
---@field to OrgMarkupRange
---@field char string
+---@class OrgMarkupPreparedHighlight
+---@field start_line number
+---@field start_col number
+---@field end_col number
+---@field hl_group string
+---@field spell? boolean
+---@field priority number
+---@field conceal? boolean
+---@field ephemeral boolean
+---@field url? string
+
---@class OrgMarkupHighlighter
---@field parse_node fun(self: OrgMarkupHighlighter, node: TSNode): OrgMarkupNode | false
---@field is_valid_start_node fun(self: OrgMarkupHighlighter, entry: OrgMarkupNode, bufnr: number): boolean
---@field is_valid_end_node fun(self: OrgMarkupHighlighter, entry: OrgMarkupNode, bufnr: number): boolean
---@field highlight fun(self: OrgMarkupHighlighter, highlights: OrgMarkupHighlight[], bufnr: number)
+---@field prepare_highlights fun(self: OrgMarkupHighlighter, highlights: OrgMarkupHighlight[], source: number | string): OrgMarkupPreparedHighlight[]
diff --git a/lua/orgmode/colors/highlighter/markup/dates.lua b/lua/orgmode/colors/highlighter/markup/dates.lua
index 6e13b4afe..8f82ca50f 100644
--- a/lua/orgmode/colors/highlighter/markup/dates.lua
+++ b/lua/orgmode/colors/highlighter/markup/dates.lua
@@ -141,6 +141,24 @@ function OrgDates:highlight(highlights, bufnr)
end
end
+---@param highlights OrgMarkupHighlight[]
+---@return OrgMarkupPreparedHighlight[]
+function OrgDates:prepare_highlights(highlights)
+ local ephemeral = self.markup:use_ephemeral()
+ local extmarks = {}
+ for _, entry in ipairs(highlights) do
+ table.insert(extmarks, {
+ start_line = entry.from.line,
+ start_col = entry.from.start_col,
+ end_col = entry.to.end_col,
+ ephemeral = ephemeral,
+ hl_group = entry.char == '>' and '@org.timestamp.active' or '@org.timestamp.inactive',
+ priority = 110,
+ })
+ end
+ return extmarks
+end
+
---@param item OrgMarkupNode
---@return boolean
function OrgDates:has_valid_parent(item)
diff --git a/lua/orgmode/colors/highlighter/markup/emphasis.lua b/lua/orgmode/colors/highlighter/markup/emphasis.lua
index 242dc2f2a..33d6f0b2c 100644
--- a/lua/orgmode/colors/highlighter/markup/emphasis.lua
+++ b/lua/orgmode/colors/highlighter/markup/emphasis.lua
@@ -88,6 +88,54 @@ function OrgEmphasis:highlight(highlights, bufnr)
end
end
+---@param highlights OrgMarkupHighlight[]
+---@return OrgMarkupPreparedHighlight[]
+function OrgEmphasis:prepare_highlights(highlights)
+ local hide_markers = config.org_hide_emphasis_markers
+ local ephemeral = self.markup:use_ephemeral()
+ local conceal = hide_markers and '' or nil
+ local extmarks = {}
+
+ for _, entry in ipairs(highlights) do
+ -- Leading delimiter
+ table.insert(extmarks, {
+ start_line = entry.from.line,
+ start_col = entry.from.start_col,
+ end_col = entry.from.end_col,
+ ephemeral = ephemeral,
+ hl_group = markers[entry.char].hl_name .. '.delimiter',
+ spell = markers[entry.char].spell,
+ priority = 110 + entry.from.start_col,
+ conceal = conceal,
+ })
+
+ -- Closing delimiter
+ table.insert(extmarks, {
+ start_line = entry.from.line,
+ start_col = entry.to.start_col,
+ ephemeral = ephemeral,
+ end_col = entry.to.end_col,
+ hl_group = markers[entry.char].hl_name .. '.delimiter',
+ spell = markers[entry.char].spell,
+ priority = 110 + entry.from.start_col,
+ conceal = conceal,
+ })
+
+ -- Main body highlight
+ table.insert(extmarks, {
+ start_line = entry.from.line,
+ start_col = entry.from.start_col + 1,
+ ephemeral = ephemeral,
+ end_col = entry.to.end_col - 1,
+ hl_group = markers[entry.char].hl_name,
+ spell = markers[entry.char].spell,
+ priority = 110 + entry.from.start_col,
+ })
+ end
+
+ return extmarks
+end
+
---@param node TSNode
---@return OrgMarkupNode | false
function OrgEmphasis:parse_node(node)
@@ -110,10 +158,10 @@ function OrgEmphasis:parse_node(node)
end
---@param entry OrgMarkupNode
----@param bufnr number
+---@param source number | string
---@return boolean
-function OrgEmphasis:is_valid_start_node(entry, bufnr)
- local start_text = self.markup:get_node_text(entry.node, bufnr, -1, 1)
+function OrgEmphasis:is_valid_start_node(entry, source)
+ local start_text = self.markup:get_node_text(entry.node, source, -1, 1)
local start_len = start_text:len()
return (start_len < 3 or vim.tbl_contains(valid_pre_marker_chars, start_text:sub(1, 1)))
@@ -121,10 +169,10 @@ function OrgEmphasis:is_valid_start_node(entry, bufnr)
end
---@param entry OrgMarkupNode
----@param bufnr number
+---@param source number | string
---@return boolean
-function OrgEmphasis:is_valid_end_node(entry, bufnr)
- local end_text = self.markup:get_node_text(entry.node, bufnr, -1, 1)
+function OrgEmphasis:is_valid_end_node(entry, source)
+ local end_text = self.markup:get_node_text(entry.node, source, -1, 1)
return (end_text:len() < 3 or vim.tbl_contains(valid_post_marker_chars, end_text:sub(3, 3)))
and end_text:sub(1, 1) ~= ' '
end
diff --git a/lua/orgmode/colors/highlighter/markup/init.lua b/lua/orgmode/colors/highlighter/markup/init.lua
index 0dcfb11cf..b3192775e 100644
--- a/lua/orgmode/colors/highlighter/markup/init.lua
+++ b/lua/orgmode/colors/highlighter/markup/init.lua
@@ -39,11 +39,10 @@ function OrgMarkup:on_line(bufnr, line, tree)
end
end
----@private
---@param bufnr number
---@param line number
---@param tree TSTree
----@return { emphasis: OrgMarkupHighlight[], link: OrgMarkupHighlight[], latex: OrgMarkupHighlight[] }
+---@return { emphasis: OrgMarkupHighlight[], link: OrgMarkupHighlight[], latex: OrgMarkupHighlight[], date: OrgMarkupHighlight[] }
function OrgMarkup:_get_highlights(bufnr, line, tree)
local line_content = vim.api.nvim_buf_get_lines(bufnr, line, line + 1, false)[1]
@@ -51,6 +50,25 @@ function OrgMarkup:_get_highlights(bufnr, line, tree)
return self.cache[bufnr][line].highlights
end
+ local result = self:get_node_highlights(tree:root(), bufnr, line)
+
+ if not self.cache[bufnr] then
+ self.cache[bufnr] = {}
+ end
+
+ self.cache[bufnr][line] = {
+ line_content = line_content,
+ highlights = result,
+ }
+
+ return result
+end
+
+---@param root_node TSNode
+---@param source number | string
+---@param line number
+---@return { emphasis: OrgMarkupHighlight[], link: OrgMarkupHighlight[], latex: OrgMarkupHighlight[], date: OrgMarkupHighlight[] }
+function OrgMarkup:get_node_highlights(root_node, source, line)
local result = {
emphasis = {},
link = {},
@@ -60,7 +78,7 @@ function OrgMarkup:_get_highlights(bufnr, line, tree)
---@type OrgMarkupNode[]
local entries = {}
- for _, node in self.query:iter_captures(tree:root(), bufnr, line, line + 1) do
+ for _, node in self.query:iter_captures(root_node, source, line, line + 1) do
local entry = nil
for _, parser in pairs(self.parsers) do
entry = parser:parse_node(node)
@@ -86,7 +104,7 @@ function OrgMarkup:_get_highlights(bufnr, line, tree)
if not self:has_valid_parent(item) then
return false
end
- return self.parsers[item.type]:is_valid_start_node(item, bufnr)
+ return self.parsers[item.type]:is_valid_start_node(item, source)
end
local is_valid_end_item = function(item)
@@ -94,7 +112,7 @@ function OrgMarkup:_get_highlights(bufnr, line, tree)
return false
end
- return self.parsers[item.type]:is_valid_end_node(item, bufnr)
+ return self.parsers[item.type]:is_valid_end_node(item, source)
end
for _, item in ipairs(entries) do
@@ -139,14 +157,26 @@ function OrgMarkup:_get_highlights(bufnr, line, tree)
::continue::
end
- if not self.cache[bufnr] then
- self.cache[bufnr] = {}
- end
+ return result
+end
- self.cache[bufnr][line] = {
- line_content = line_content,
- highlights = result,
- }
+---@param headline OrgHeadline
+---@return OrgMarkupPreparedHighlight[]
+function OrgMarkup:get_prepared_headline_highlights(headline)
+ local highlights =
+ self:get_node_highlights(headline:node(), headline.file:get_source(), select(1, headline:node():range()))
+
+ local result = {}
+
+ for type, highlight in pairs(highlights) do
+ vim.list_extend(
+ result,
+ self.parsers[type]:prepare_highlights(highlight, function(markup_highlight)
+ local text = headline.file:get_node_text(headline:node())
+ return text:sub(markup_highlight.from.start_col + 1, markup_highlight.to.end_col)
+ end)
+ )
+ end
return result
end
@@ -156,7 +186,7 @@ function OrgMarkup:on_detach(bufnr)
end
---@param node TSNode
----@param source number
+---@param source number | string
---@param offset_col_start? number
---@param offset_col_end? number
---@return string
diff --git a/lua/orgmode/colors/highlighter/markup/latex.lua b/lua/orgmode/colors/highlighter/markup/latex.lua
index 68bbc2d4e..bef9f887c 100644
--- a/lua/orgmode/colors/highlighter/markup/latex.lua
+++ b/lua/orgmode/colors/highlighter/markup/latex.lua
@@ -104,4 +104,23 @@ function OrgLatex:highlight(highlights, bufnr)
end
end
+---@param highlights OrgMarkupHighlight[]
+---@return OrgMarkupPreparedHighlight[]
+function OrgLatex:prepare_highlights(highlights)
+ local ephemeral = self.markup:use_ephemeral()
+ local extmarks = {}
+ for _, entry in ipairs(highlights) do
+ table.insert(extmarks, {
+ start_line = entry.from.line,
+ start_col = entry.from.start_col,
+ end_col = entry.to.end_col,
+ ephemeral = ephemeral,
+ hl_group = '@org.latex',
+ spell = false,
+ priority = 110 + entry.from.start_col,
+ })
+ end
+ return extmarks
+end
+
return OrgLatex
diff --git a/lua/orgmode/colors/highlighter/markup/link.lua b/lua/orgmode/colors/highlighter/markup/link.lua
index 6c015f080..024991684 100644
--- a/lua/orgmode/colors/highlighter/markup/link.lua
+++ b/lua/orgmode/colors/highlighter/markup/link.lua
@@ -1,6 +1,3 @@
-local ts_utils = require('orgmode.utils.treesitter')
-local utils = require('orgmode.utils')
-
---@class OrgLinkHighlighter : OrgMarkupHighlighter
---@field private markup OrgMarkupHighlighter
---@field private has_extmark_url_support boolean
@@ -136,4 +133,65 @@ function OrgLink:highlight(highlights, bufnr)
end
end
+---@param highlights OrgMarkupHighlight[]
+---@param source_getter_fn fun(highlight: OrgMarkupHighlight): string
+---@return OrgMarkupPreparedHighlight[]
+function OrgLink:prepare_highlights(highlights, source_getter_fn)
+ local ephemeral = self.markup:use_ephemeral()
+ local extmarks = {}
+
+ for _, entry in ipairs(highlights) do
+ local link = source_getter_fn(entry)
+ local alias = link:find('%]%[') or 1
+ local link_end = link:find('%]%[') or (link:len() - 1)
+
+ local link_opts = {
+ ephemeral = ephemeral,
+ end_col = entry.to.end_col,
+ hl_group = '@org.hyperlink',
+ priority = 110,
+ }
+
+ if self.has_extmark_url_support then
+ link_opts.url = alias > 1 and link:sub(3, alias - 1) or link:sub(3, -3)
+ end
+
+ table.insert(extmarks, {
+ start_line = entry.from.line,
+ start_col = entry.from.start_col,
+ end_col = link_opts.end_col,
+ ephemeral = link_opts.ephemeral,
+ hl_group = link_opts.hl_group,
+ priority = link_opts.priority,
+ url = link_opts.url,
+ })
+
+ table.insert(extmarks, {
+ start_line = entry.from.line,
+ start_col = entry.from.start_col,
+ end_col = entry.from.start_col + 1 + alias,
+ ephemeral = ephemeral,
+ conceal = '',
+ })
+
+ table.insert(extmarks, {
+ start_line = entry.from.line,
+ start_col = entry.from.start_col + 2,
+ end_col = entry.from.start_col - 1 + link_end,
+ ephemeral = ephemeral,
+ spell = false,
+ })
+
+ table.insert(extmarks, {
+ start_line = entry.from.line,
+ start_col = entry.to.end_col - 2,
+ end_col = entry.to.end_col,
+ ephemeral = ephemeral,
+ conceal = '',
+ })
+ end
+
+ return extmarks
+end
+
return OrgLink
diff --git a/lua/orgmode/colors/init.lua b/lua/orgmode/colors/init.lua
index 7a04b8940..dd8025fb4 100644
--- a/lua/orgmode/colors/init.lua
+++ b/lua/orgmode/colors/init.lua
@@ -85,6 +85,16 @@ M.highlight = function(highlights, clear, bufnr)
end_line = hl.range.start_line,
hl_eol = true,
})
+ elseif hl.extmark then
+ vim.api.nvim_buf_set_extmark(bufnr, namespace, hl.range.start_line - 1, hl.range.start_col - 1, {
+ hl_group = hl.hlgroup,
+ end_line = hl.range.end_line - 1,
+ end_col = hl.range.end_col - 1,
+ spell = hl.spell,
+ priority = hl.priority,
+ conceal = hl.conceal,
+ url = hl.url,
+ })
else
vim.api.nvim_buf_add_highlight(
bufnr,
@@ -128,11 +138,11 @@ M.clear_extmarks = function(bufnr, start_line, end_line)
end
end
-M.add_hr = function(bufnr, line)
+M.add_hr = function(bufnr, line, separator)
vim.api.nvim_buf_set_lines(bufnr, line, line, false, { '' })
local width = vim.api.nvim_win_get_width(0)
vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, {
- virt_text = { { string.rep('-', width), '@org.agenda.separator' } },
+ virt_text = { { string.rep(separator, width), '@org.agenda.separator' } },
virt_text_pos = 'overlay',
})
end
diff --git a/lua/orgmode/config/defaults.lua b/lua/orgmode/config/defaults.lua
index 7414443dd..b08ae5c12 100644
--- a/lua/orgmode/config/defaults.lua
+++ b/lua/orgmode/config/defaults.lua
@@ -1,10 +1,38 @@
---@alias OrgAgendaSpan 'day' | 'week' | 'month' | 'year' | number
----
+
+---@class OrgAgendaCustomCommandTypeInterface
+---@field type? 'agenda' | 'tags' | 'tags_todo'
+---@field org_agenda_overriding_header? string
+---@field org_agenda_files? string[]
+---@field org_agenda_tag_filter_preset? string
+---@field org_agenda_category_filter_preset? string
+---@field org_agenda_sorting_strategy? OrgAgendaSortingStrategy[]
+---@field org_agenda_remove_tags? boolean
+
+---@class OrgAgendaCustomCommandAgenda:OrgAgendaCustomCommandTypeInterface
+---@field org_agenda_span? OrgAgendaSpan Default: 'week'
+---@field org_agenda_start_day? string Modifier from today, example '+1d'
+---@field org_agenda_start_on_weekday? number
+
+---@class OrgAgendaCustomCommandTags:OrgAgendaCustomCommandTypeInterface
+---@field match? string
+---@field org_agenda_todo_ignore_scheduled? OrgAgendaTodoIgnoreScheduledTypes
+---@field org_agenda_todo_ignore_deadlines? OrgAgendaTodoIgnoreDeadlinesTypes
+
+---@alias OrgAgendaCustomCommandType (OrgAgendaCustomCommandAgenda | OrgAgendaCustomCommandTags)
+
+---@class OrgAgendaCustomCommand
+---@field description string Description in prompt
+---@field types? OrgAgendaCustomCommandType[]
+
---@class OrgDefaultConfig
---@field org_id_method 'uuid' | 'ts' | 'org'
---@field org_agenda_span OrgAgendaSpan
---@field org_log_repeat 'time' | 'note' | false
---@field calendar { round_min_with_hours: boolean, min_big_step: number, min_small_step: number? }
+---@field org_agenda_custom_commands table
+---@field org_agenda_sorting_strategy table<'agenda' | 'todo' | 'tags', OrgAgendaSortingStrategy[]>
+---@field org_agenda_remove_tags? boolean
local DefaultConfig = {
org_agenda_files = '',
org_default_notes_file = '',
@@ -31,6 +59,14 @@ local DefaultConfig = {
org_agenda_skip_scheduled_if_done = false,
org_agenda_skip_deadline_if_done = false,
org_agenda_text_search_extra_files = {},
+ org_agenda_custom_commands = {},
+ org_agenda_block_separator = '-',
+ org_agenda_sorting_strategy = {
+ agenda = { 'time-up', 'priority-down', 'category-keep' },
+ todo = { 'priority-down', 'category-keep' },
+ tags = { 'priority-down', 'category-keep' },
+ },
+ org_agenda_remove_tags = false,
org_priority_highest = 'A',
org_priority_default = 'B',
org_priority_lowest = 'C',
@@ -201,7 +237,7 @@ local DefaultConfig = {
},
emacs_config = {
executable_path = 'emacs',
- config_path = '$HOME/.emacs.d/init.el',
+ config_path = nil,
},
ui = {
folds = {
diff --git a/lua/orgmode/export/init.lua b/lua/orgmode/export/init.lua
index d2aa6d3c9..ac0ff533c 100644
--- a/lua/orgmode/export/init.lua
+++ b/lua/orgmode/export/init.lua
@@ -69,11 +69,26 @@ function Export.pandoc(opts)
end
---@param opts table
-function Export.emacs(opts)
+---@param skip_config? boolean
+function Export.emacs(opts, skip_config)
local file = utils.current_file_path()
local target = vim.fn.fnamemodify(file, ':p:r') .. '.' .. opts.extension
local emacs = config.emacs_config.executable_path
local emacs_config_path = config.emacs_config.config_path
+ if not emacs_config_path and not skip_config then
+ local paths = {
+ '~/.config/emacs/init.el',
+ '~/.emacs.d/init.el',
+ '~/.emacs.el',
+ }
+ for _, path in ipairs(paths) do
+ if vim.uv.fs_stat(vim.fn.fnamemodify(path, ':p')) then
+ emacs_config_path = vim.fn.fnamemodify(path, ':p')
+ break
+ end
+ end
+ end
+
if vim.fn.executable(emacs) ~= 1 then
return utils.echo_error('emacs executable not found. Make sure emacs is in $PATH.')
end
@@ -82,16 +97,25 @@ function Export.emacs(opts)
emacs,
'-nw',
'--batch',
- '--load',
- emacs_config_path,
- string.format('--visit=%s', file),
- string.format('--funcall=%s', opts.command),
}
+ if emacs_config_path and not skip_config then
+ table.insert(cmd, '--load')
+ table.insert(cmd, emacs_config_path)
+ end
+
+ table.insert(cmd, ('--visit=%s'):format(file))
+ table.insert(cmd, ('--funcall=%s'):format(opts.command))
+
return Export._exporter(cmd, target, nil, function(err)
table.insert(err, '')
table.insert(err, 'NOTE: Emacs export issues are most likely caused by bad or missing emacs configuration.')
- return utils.echo_error(string.format('Export error:\n%s', table.concat(err, '\n')))
+ utils.echo_error(string.format('Export error:\n%s', table.concat(err, '\n')))
+ if not skip_config then
+ if vim.fn.input('Attempt to export again without a configuration file? [y/n]') == 'y' then
+ return Export.emacs(opts, true)
+ end
+ end
end)
end
diff --git a/lua/orgmode/files/file.lua b/lua/orgmode/files/file.lua
index 1102cd8dd..1c4d23ce2 100644
--- a/lua/orgmode/files/file.lua
+++ b/lua/orgmode/files/file.lua
@@ -20,6 +20,7 @@ local Memoize = require('orgmode.utils.memoize')
---@class OrgFile
---@field filename string
+---@field index number
---@field lines string[]
---@field content string
---@field metadata OrgFileMetadata
@@ -43,6 +44,7 @@ function OrgFile:new(opts)
filename = opts.filename,
lines = opts.lines,
content = table.concat(opts.lines, '\n'),
+ index = 0,
metadata = {
mtime = stat and stat.mtime.nsec or 0,
changedtick = opts.bufnr and vim.api.nvim_buf_get_changedtick(opts.bufnr) or 0,
@@ -173,7 +175,7 @@ function OrgFile:get_ts_matches(query, node)
local matches = {}
local from, _, to = node:range()
- for _, match, _ in ts_query:iter_matches(node, self:_get_source(), from, to + 1, { all = false }) do
+ for _, match, _ in ts_query:iter_matches(node, self:get_source(), from, to + 1, { all = false }) do
local items = {}
for id, matched_nodes in pairs(match) do
local name = ts_query.captures[id]
@@ -285,6 +287,7 @@ function OrgFile:apply_search(search, todo_only)
scheduled = scheduled and scheduled:to_wrapped_string(true),
closed = closed and closed:to_wrapped_string(false),
priority = priority,
+ todo = item:get_todo() or '',
}),
tags = item:get_tags(),
todo = item:get_todo() or '',
@@ -419,13 +422,13 @@ function OrgFile:get_node_text(node, range)
return ''
end
if range then
- return ts.get_node_text(node, self:_get_source(), {
+ return ts.get_node_text(node, self:get_source(), {
metadata = {
range = range,
},
})
end
- return ts.get_node_text(node, self:_get_source())
+ return ts.get_node_text(node, self:get_source())
end
---@param node? TSNode
@@ -736,7 +739,7 @@ function OrgFile:get_links()
local links = {}
local processed_lines = {}
- for _, match in ts_query:iter_captures(self.root, self:_get_source()) do
+ for _, match in ts_query:iter_captures(self.root, self:get_source()) do
local line = match:start()
if not processed_lines[line] then
vim.list_extend(links, Hyperlink.all_from_line(self.lines[line + 1], line + 1))
@@ -831,9 +834,8 @@ end
--- Get the ts source for the file
--- If there is a buffer, return buffer number
--- Otherwise, return the string content
----@private
---@return integer | string
-function OrgFile:_get_source()
+function OrgFile:get_source()
local bufnr = self:bufnr()
if bufnr > -1 then
return bufnr
diff --git a/lua/orgmode/files/headline.lua b/lua/orgmode/files/headline.lua
index 1f5801504..d68f99299 100644
--- a/lua/orgmode/files/headline.lua
+++ b/lua/orgmode/files/headline.lua
@@ -14,6 +14,7 @@ local Memoize = require('orgmode.utils.memoize')
---@class OrgHeadline
---@field headline TSNode
---@field file OrgFile
+---@field index? number
local Headline = {}
local memoize = Memoize:new(Headline, function(self)
@@ -326,8 +327,8 @@ end
memoize('get_todo')
--- Returns the headlines todo keyword, it's node,
---- and it's type (todo or done)
---- @return string | nil, TSNode | nil, string | nil
+--- it's type (todo or done) and it's index in the todo_keywords list
+--- @return string | nil, TSNode | nil, string | nil, number | nil
function Headline:get_todo()
-- A valid keyword can only be the first child
local first_item_node = self:_get_child_node('item')
@@ -341,10 +342,10 @@ function Headline:get_todo()
local text = self.file:get_node_text(todo_node)
local keyword_by_value = todo_keywords:find(text)
if not keyword_by_value then
- return nil, nil, nil
+ return nil, nil, nil, nil
end
- return text, todo_node, keyword_by_value.type
+ return text, todo_node, keyword_by_value.type, keyword_by_value.index
end
---@return boolean
@@ -360,18 +361,24 @@ function Headline:is_done()
end
memoize('get_title')
----@return string
+---@return string, number
function Headline:get_title()
- local title = self.file:get_node_text(self:_get_child_node('item')) or ''
+ local title_node = self:_get_child_node('item')
+ local title = self.file:get_node_text(title_node) or ''
local word, todo_node = self:get_todo()
+ local offset = title_node and select(2, title_node:start()) or 0
if todo_node and word then
- title = title:gsub('^' .. vim.pesc(word) .. '%s*', '')
+ local new_title = title:gsub('^' .. vim.pesc(word) .. '%s*', '')
+ offset = offset + (title:len() - new_title:len())
+ title = new_title
end
local priority, priority_node = self:get_priority()
if priority_node then
- title = title:gsub('^' .. vim.pesc(('[#%s]'):format(priority)) .. '%s*', '')
+ local new_title = title:gsub('^' .. vim.pesc(('[#%s]'):format(priority)) .. '%s*', '')
+ offset = offset + title:len() - new_title:len()
+ title = new_title
end
- return title
+ return title, offset
end
function Headline:get_title_with_priority()
@@ -725,9 +732,11 @@ function Headline:get_non_plan_dates()
return dates
end
-function Headline:tags_to_string()
+---@param sorted? boolean
+---@return string, TSNode | nil
+function Headline:tags_to_string(sorted)
local tags, node = self:get_tags()
- return utils.tags_to_string(tags), node
+ return utils.tags_to_string(tags, sorted), node
end
---@return boolean
diff --git a/lua/orgmode/files/init.lua b/lua/orgmode/files/init.lua
index e3c8da330..def981feb 100644
--- a/lua/orgmode/files/init.lua
+++ b/lua/orgmode/files/init.lua
@@ -7,28 +7,48 @@ local Listitem = require('orgmode.files.elements.listitem')
---@class OrgFilesOpts
---@field paths string | string[]
+---@field cache? boolean Store the instances to cache and retrieve it later if paths are the same
---@class OrgLoadFileOpts
---@field persist boolean Persist the file in the list of loaded files if it belongs to path
---@class OrgFiles
+---@field cache? boolean
+---@field cached_instances table
---@field paths string[]
---@field files table table with files that are part of paths
---@field all_files table all loaded files, no matter if they are part of paths
---@field load_state 'loading' | 'loaded' | nil
-local OrgFiles = {}
+local OrgFiles = {
+ cached_instances = {},
+}
OrgFiles.__index = OrgFiles
---@param opts OrgFilesOpts
+---@return OrgFiles
function OrgFiles:new(opts)
local data = {
files = {},
all_files = {},
load_state = nil,
+ cache = opts.cache or false,
}
setmetatable(data, self)
data.paths = self:_setup_paths(opts.paths)
- return data
+ return data:cache_and_return()
+end
+
+function OrgFiles:cache_and_return()
+ if not self.cache then
+ return self
+ end
+ local key = table.concat(self.paths)
+ local cached = OrgFiles.cached_instances[key]
+ if cached then
+ return cached
+ end
+ OrgFiles.cached_instances[key] = self
+ return self
end
---@param force? boolean Force reload all files
@@ -42,9 +62,10 @@ function OrgFiles:load(force)
end
self.load_state = 'loading'
- return Promise.map(function(filename)
+ return Promise.map(function(filename, index)
return self:load_file(filename):next(function(orgfile)
if orgfile then
+ orgfile.index = index
self.files[orgfile.filename] = orgfile
end
return orgfile
@@ -128,8 +149,9 @@ function OrgFiles:all()
self:ensure_loaded()
local valid_files = {}
local filenames = self:_files()
- for _, file in ipairs(filenames) do
+ for i, file in ipairs(filenames) do
if self.files[file] then
+ self.files[file].index = i
table.insert(valid_files, self.files[file])
end
end
diff --git a/lua/orgmode/init.lua b/lua/orgmode/init.lua
index 08fdd5249..f2f9e6993 100644
--- a/lua/orgmode/init.lua
+++ b/lua/orgmode/init.lua
@@ -56,6 +56,7 @@ function Org:init()
self.links = require('orgmode.org.links'):new({ files = self.files })
self.agenda = require('orgmode.agenda'):new({
files = self.files,
+ highlighter = self.highlighter,
})
self.capture = require('orgmode.capture'):new({
files = self.files,
diff --git a/lua/orgmode/objects/date.lua b/lua/orgmode/objects/date.lua
index 2ced5ad2b..9c7ea4bc8 100644
--- a/lua/orgmode/objects/date.lua
+++ b/lua/orgmode/objects/date.lua
@@ -713,6 +713,15 @@ function Date:get_date_range_end()
return self:has_date_range_end() and self.related_date_range or nil
end
+function Date:get_type_sort_value()
+ local types = {
+ DEADLINE = 1,
+ SCHEDULED = 2,
+ NONE = 3,
+ }
+ return types[self.type]
+end
+
---Return number of days for a date range
---@return number
function Date:get_date_range_days()
diff --git a/lua/orgmode/org/links/init.lua b/lua/orgmode/org/links/init.lua
index c6d37df24..5d186dc76 100644
--- a/lua/orgmode/org/links/init.lua
+++ b/lua/orgmode/org/links/init.lua
@@ -96,9 +96,9 @@ function OrgLinks:get_link_to_file(file)
end
---@param link_location string
-function OrgLinks:insert_link(link_location)
+function OrgLinks:insert_link(link_location, desc)
local selected_link = OrgHyperlink:new(link_location)
- local desc = selected_link.url:get_target()
+ desc = desc or selected_link.url:get_target()
if desc and (desc:match('^%*') or desc:match('^#')) then
desc = desc:sub(2)
end
diff --git a/lua/orgmode/org/mappings.lua b/lua/orgmode/org/mappings.lua
index ae63928e2..fd4673cf7 100644
--- a/lua/orgmode/org/mappings.lua
+++ b/lua/orgmode/org/mappings.lua
@@ -785,7 +785,8 @@ end
-- Inserts a new link after the cursor position or modifies the link the cursor is
-- currently on
function OrgMappings:insert_link()
- local link_location = vim.fn.OrgmodeInput('Links: ', '', function(arg_lead)
+ local link = OrgHyperlink.at_cursor()
+ local link_location = vim.fn.OrgmodeInput('Links: ', link and link.url:to_string() or '', function(arg_lead)
return self.links:autocomplete(arg_lead)
end)
if vim.trim(link_location) == '' then
@@ -793,7 +794,7 @@ function OrgMappings:insert_link()
return
end
- self.links:insert_link(link_location)
+ self.links:insert_link(link_location, link and link.desc)
end
function OrgMappings:store_link()
diff --git a/lua/orgmode/utils/init.lua b/lua/orgmode/utils/init.lua
index 435fcad0f..62ce636c0 100644
--- a/lua/orgmode/utils/init.lua
+++ b/lua/orgmode/utils/init.lua
@@ -252,9 +252,12 @@ function utils.parse_tags_string(tags)
return parsed_tags
end
-function utils.tags_to_string(taglist)
+function utils.tags_to_string(taglist, sorted)
local tags = ''
if #taglist > 0 then
+ if sorted then
+ table.sort(taglist)
+ end
tags = ':' .. table.concat(taglist, ':') .. ':'
end
return tags