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