From 3669bc069dd415fc696da48cd93345804322d623 Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Wed, 21 Feb 2024 23:20:55 +0100 Subject: [PATCH] refactor(completion)!: Extract sources to separate classes and remove nvim-compe support (#678) --- DOCS.md | 9 - README.md | 14 - ftplugin/org.lua | 2 +- lua/orgmode/init.lua | 4 +- lua/orgmode/objects/url.lua | 42 +- lua/orgmode/org/autocompletion/_meta.lua | 9 + lua/orgmode/org/autocompletion/cmp.lua | 11 +- lua/orgmode/org/autocompletion/compe.lua | 42 -- lua/orgmode/org/autocompletion/init.lua | 109 ++++- lua/orgmode/org/autocompletion/omni.lua | 184 -------- .../org/autocompletion/sources/directives.lua | 40 ++ .../org/autocompletion/sources/hyperlinks.lua | 35 ++ .../org/autocompletion/sources/plan.lua | 42 ++ .../org/autocompletion/sources/properties.lua | 42 ++ .../org/autocompletion/sources/tags.lua | 37 ++ .../autocompletion/sources/todo_keywords.lua | 30 ++ lua/orgmode/org/hyperlinks.lua | 70 ++- lua/orgmode/utils/fs.lua | 2 +- queries/org/highlights.scm | 1 + tests/plenary/org/autocompletion_spec.lua | 435 +++++++++--------- 20 files changed, 633 insertions(+), 527 deletions(-) create mode 100644 lua/orgmode/org/autocompletion/_meta.lua delete mode 100644 lua/orgmode/org/autocompletion/compe.lua delete mode 100644 lua/orgmode/org/autocompletion/omni.lua create mode 100644 lua/orgmode/org/autocompletion/sources/directives.lua create mode 100644 lua/orgmode/org/autocompletion/sources/hyperlinks.lua create mode 100644 lua/orgmode/org/autocompletion/sources/plan.lua create mode 100644 lua/orgmode/org/autocompletion/sources/properties.lua create mode 100644 lua/orgmode/org/autocompletion/sources/tags.lua create mode 100644 lua/orgmode/org/autocompletion/sources/todo_keywords.lua diff --git a/DOCS.md b/DOCS.md index 65be6a5fd..e4b598527 100644 --- a/DOCS.md +++ b/DOCS.md @@ -1248,15 +1248,6 @@ By default, `omnifunc` is provided in `org` files that autocompletes these types * Orgfile special keywords (`#+TITLE`, `#+BEGIN_SRC`, `#+ARCHIVE`, etc.) * Hyperlinks (`* - headlines`, `# - headlines with CUSTOM_ID property`, `headlines matching title`) -If you use [nvim-compe](https://github.com/hrsh7th/nvim-compe) add this to compe setup: -```lua - require'compe'.setup({ - source = { - orgmode = true - } - }) -``` - For [nvim-cmp](https://github.com/hrsh7th/nvim-cmp), add `orgmode` to list of sources: ```lua require'cmp'.setup({ diff --git a/README.md b/README.md index bbebc300f..366287597 100644 --- a/README.md +++ b/README.md @@ -132,20 +132,6 @@ EOF ``` #### Completion -
- nvim-compe -
- -```lua -require('compe').setup({ - source = { - orgmode = true - } -}) -``` - -
-
nvim-cmp
diff --git a/ftplugin/org.lua b/ftplugin/org.lua index c93a67bfc..1a6955bef 100644 --- a/ftplugin/org.lua +++ b/ftplugin/org.lua @@ -31,7 +31,7 @@ vim.opt_local.omnifunc = 'v:lua.orgmode.omnifunc' vim.opt_local.commentstring = '# %s' _G.orgmode.omnifunc = function(findstart, base) - return require('orgmode.org.autocompletion.omni').omnifunc(findstart, base) + return require('orgmode').completion:omnifunc(findstart, base) end local abbreviations = { diff --git a/lua/orgmode/init.lua b/lua/orgmode/init.lua index fbd43a19d..0cff73a4a 100644 --- a/lua/orgmode/init.lua +++ b/lua/orgmode/init.lua @@ -11,6 +11,7 @@ local auto_instance_keys = { clock = true, org_mappings = true, notifications = true, + completion = true, } ---@class Org @@ -20,6 +21,7 @@ local auto_instance_keys = { ---@field agenda OrgAgenda ---@field capture OrgCapture ---@field clock OrgClock +---@field completion OrgCompletion ---@field org_mappings OrgMappings ---@field notifications OrgNotifications local Org = {} @@ -62,7 +64,7 @@ function Org:init() self.clock = require('orgmode.clock'):new({ files = self.files, }) - require('orgmode.org.autocompletion').register() + self.completion = require('orgmode.org.autocompletion'):new({ files = self.files }) self.statusline_debounced = require('orgmode.utils').debounce('statusline', function() return self.clock:get_statusline() end, 300) diff --git a/lua/orgmode/objects/url.lua b/lua/orgmode/objects/url.lua index 62af202f7..32b5bdee7 100644 --- a/lua/orgmode/objects/url.lua +++ b/lua/orgmode/objects/url.lua @@ -3,15 +3,10 @@ local fs = require('orgmode.utils.fs') ---@class OrgUrl ---@field str string local Url = {} - -function Url:init(str) - self.str = str -end +Url.__index = Url function Url.new(str) - local self = setmetatable({}, { __index = Url }) - self:init(str) - return self + return setmetatable({ str = str }, Url) end ---@return boolean @@ -44,11 +39,6 @@ function Url:is_file_custom_id() return self:is_file() and self:get_custom_id() and true or false end ----@return boolean -function Url:is_file_anchor() - return self:get_dedicated_target() and true -end - ---@return boolean function Url:is_org_link() return (self:get_dedicated_target() or self:get_custom_id() or self:get_headline()) and true @@ -77,7 +67,7 @@ function Url:is_internal_custom_id() end function Url:is_dedicated_anchor_or_internal_title() - return self:get_dedicated_target() ~= nil + return self:get_dedicated_target() end ---@return string | false @@ -145,6 +135,11 @@ function Url:get_linenumber() or self.str:match('^/[^:]+ %+(%d+)$') end +---@return string | false +function Url:get_protocol() + return self.str:match('^([%w]+):') +end + ---@return string | false function Url:get_filepath() return @@ -166,6 +161,27 @@ function Url:get_filepath() or self.str:match('^(%./)$') or self.str:match('^(/)$') end + +function Url:get_file() + return + -- for backwards compatibility + self.str:match('^(file:[^:]+) %+%d+') + or self.str:match('^(%.%./[^:]+) %+%d+') + or self.str:match('^(%./[^:]+) %+%d+') + or self.str:match('^(/[^:]+) %+%d+') + -- official orgmode convention + or self.str:match('^(file:[^:]+)::') + or self.str:match('^(%.%./[^:]+)::') + or self.str:match('^(%./[^:]+)::') + or self.str:match('^(/[^:]+)::') + or self.str:match('^(file:[^:]+)$') + or self.str:match('^(%.%./[^:]+)$') + or self.str:match('^(%./[^:]+)$') + or self.str:match('^(/[^:]+)$') + or self.str:match('^(%.%./)$') + or self.str:match('^(%./)$') + or self.str:match('^(/)$') +end -- ---@return string function Url:get_headline_completion() diff --git a/lua/orgmode/org/autocompletion/_meta.lua b/lua/orgmode/org/autocompletion/_meta.lua new file mode 100644 index 000000000..d73d752cf --- /dev/null +++ b/lua/orgmode/org/autocompletion/_meta.lua @@ -0,0 +1,9 @@ +---@meta + +---@alias OrgCompletionContext { line: string, base?: string } +---@alias OrgCompletionItem { word: string, menu: string } + +---@class OrgCompletionSource +---@field get_name fun(self: OrgCompletionSource): string +---@field get_start fun(self: OrgCompletionSource, context: OrgCompletionContext): number | nil +---@field get_results fun(self: OrgCompletionSource, context: OrgCompletionContext): string[] diff --git a/lua/orgmode/org/autocompletion/cmp.lua b/lua/orgmode/org/autocompletion/cmp.lua index f62450a70..fcac2a90b 100644 --- a/lua/orgmode/org/autocompletion/cmp.lua +++ b/lua/orgmode/org/autocompletion/cmp.lua @@ -3,7 +3,7 @@ if not has_cmp then return end -local Omni = require('orgmode.org.autocompletion.omni') +local org = require('orgmode') local Source = {} @@ -25,9 +25,12 @@ function Source:get_trigger_characters(_) end function Source:complete(params, callback) - local offset = Omni.find_start() + 1 - local input = string.sub(params.context.cursor_before_line, offset) - local results = Omni.get_completions(input) + local offset = org.completion:get_start({ line = params.context.cursor_before_line }) + 1 + local base = string.sub(params.context.cursor_before_line, offset) + local results = org.completion:complete({ + line = params.context.cursor_before_line, + base = base, + }) local items = {} for _, item in ipairs(results) do table.insert(items, { diff --git a/lua/orgmode/org/autocompletion/compe.lua b/lua/orgmode/org/autocompletion/compe.lua deleted file mode 100644 index f844246d9..000000000 --- a/lua/orgmode/org/autocompletion/compe.lua +++ /dev/null @@ -1,42 +0,0 @@ -local has_compe, compe = pcall(require, 'compe') -if not has_compe then - return -end - -local Omni = require('orgmode.org.autocompletion.omni') - -local CompeSource = {} - -function CompeSource.new() - return setmetatable({}, { __index = CompeSource }) -end - -function CompeSource.get_metadata() - return { - priority = 999, - sort = false, - dup = 0, - filetypes = { 'org' }, - menu = '[Org]', - } -end - -function CompeSource.determine(_, context) - local offset = Omni.find_start() + 1 - if offset > 0 then - return { - keyword_pattern_offset = offset, - trigger_character_offset = vim.tbl_contains({ '#', '+', ':', '*' }, context.before_char) and context.col or 0, - } - end -end - -function CompeSource.complete(_, context) - local items = Omni.get_completions(context.input) - context.callback({ - items = items, - incomplete = true, - }) -end - -compe.register_source('orgmode', CompeSource) diff --git a/lua/orgmode/org/autocompletion/init.lua b/lua/orgmode/org/autocompletion/init.lua index 2b37c140a..9a376c514 100644 --- a/lua/orgmode/org/autocompletion/init.lua +++ b/lua/orgmode/org/autocompletion/init.lua @@ -1,8 +1,107 @@ -local function register() - require('orgmode.org.autocompletion.compe') +---@class OrgCompletion +---@field files OrgFiles +---@field private sources OrgCompletionSource[] +---@field private sources_by_name table +---@field menu string +local OrgCompletion = { + menu = '[Org]', +} +OrgCompletion.__index = OrgCompletion + +---@param opts { files: OrgFiles } +function OrgCompletion:new(opts) + local this = setmetatable({ + files = opts.files, + sources = {}, + sources_by_name = {}, + }, OrgCompletion) + this:setup_builtin_sources() + this:register_frameworks() + return this +end + +function OrgCompletion:setup_builtin_sources() + self:add_source(require('orgmode.org.autocompletion.sources.todo_keywords'):new()) + self:add_source(require('orgmode.org.autocompletion.sources.tags'):new({ completion = self })) + self:add_source(require('orgmode.org.autocompletion.sources.plan'):new({ completion = self })) + self:add_source(require('orgmode.org.autocompletion.sources.directives'):new()) + self:add_source(require('orgmode.org.autocompletion.sources.properties'):new({ completion = self })) + self:add_source(require('orgmode.org.autocompletion.sources.hyperlinks'):new({ completion = self })) +end + +---@param source OrgCompletionSource +function OrgCompletion:add_source(source) + if self.sources_by_name[source:get_name()] then + error('Completion source ' .. source:get_name() .. ' already exists') + end + self.sources_by_name[source:get_name()] = source + table.insert(self.sources, source) +end + +---@param context OrgCompletionContext +---@return OrgCompletionItem +function OrgCompletion:complete(context) + local results = {} + for _, source in ipairs(self.sources) do + if source:get_start(context) then + vim.list_extend(results, self:_get_valid_results(source:get_results(context), context)) + end + end + + return results +end + +function OrgCompletion:_get_valid_results(results, context) + local base = context.base or '' + + local valid_results = {} + for _, item in ipairs(results) do + if base == '' or item:find('^' .. vim.pesc(base)) then + table.insert(valid_results, { + word = item, + menu = self.menu, + }) + end + end + + return valid_results +end + +---@param context OrgCompletionContext +function OrgCompletion:get_start(context) + for _, source in ipairs(self.sources) do + local start = source:get_start(context) + if start then + return start + end + end + + return -1 +end + +function OrgCompletion:omnifunc(findstart, base) + if findstart == 1 then + self._context = { line = self:get_line() } + return self:get_start(self._context) + end + + self._context = self._context or { line = self:get_line() } + self._context.base = base + return self:complete(self._context) +end + +function OrgCompletion:get_line() + local cursor = vim.api.nvim_win_get_cursor(0) + return vim.api.nvim_get_current_line():sub(1, cursor[2]) +end + +---@param line string +function OrgCompletion:is_headline_line(line) + return line:find([[^%*+%s+]]) ~= nil +end + +function OrgCompletion:register_frameworks() require('orgmode.org.autocompletion.cmp') end -return { - register = register, -} +return OrgCompletion diff --git a/lua/orgmode/org/autocompletion/omni.lua b/lua/orgmode/org/autocompletion/omni.lua deleted file mode 100644 index cd6aeb6cf..000000000 --- a/lua/orgmode/org/autocompletion/omni.lua +++ /dev/null @@ -1,184 +0,0 @@ -local org = require('orgmode') -local config = require('orgmode.config') -local Hyperlinks = require('orgmode.org.hyperlinks') -local Url = require('orgmode.objects.url') - -local data = { - directives = { '#+title', '#+author', '#+email', '#+name', '#+filetags', '#+archive', '#+options', '#+category' }, - begin_blocks = { '#+begin_src', '#+end_src', '#+begin_example', '#+end_example' }, - properties = { ':PROPERTIES:', ':END:', ':LOGBOOK:', ':STYLE:', ':REPEAT_TO_STATE:', ':CUSTOM_ID:', ':CATEGORY:' }, - metadata = { 'DEADLINE:', 'SCHEDULED:', 'CLOSED:' }, -} - -local directives = { - line_rgx = vim.regex([[^\#\?+\?\w*$]]), - rgx = vim.regex([[^\#+\?\w*$]]), - list = data.directives, -} - -local begin_blocks = { - line_rgx = vim.regex([[^\s*\#\?+\?\w*$]]), - rgx = vim.regex([[\(^\s*\)\@<=\#+\?\w*$]]), - list = data.begin_blocks, -} - -local properties = { - line_rgx = vim.regex([[\(^\s\+\|^\s*:\?$\)]]), - rgx = vim.regex([[\(^\|^\s\+\)\@<=:\w*$]]), - extra_cond = function(line, _) - return not string.find(line, 'file:.*$') - end, - list = data.properties, -} - -local links = { - line_rgx = vim.regex([[\(\(^\|\s\+\)\[\[\)\@<=\(\*\|#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\)\+\)\?]]), - rgx = vim.regex([[\(\*\|#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\)\+\)\?$]]), - fetcher = function(url) - local hyperlinks, mapper = Hyperlinks.find_matching_links(url) - return mapper(hyperlinks) - end, -} - -local metadata = { - rgx = vim.regex([[\(\s*\)\@<=\w\+$]]), - list = data.metadata, -} - -local tags = { - rgx = vim.regex([[:\([0-9A-Za-z_%@\#]*\)$]]), - fetcher = function() - return vim.tbl_map(function(tag) - return ':' .. tag .. ':' - end, org.files:get_tags() or {}) - end, -} - -local filetags = { - line_rgx = vim.regex([[\c^\#+filetags:\s\+]]), - rgx = vim.regex([[:\([0-9A-Za-z_%@\#]*\)$]]), - extra_cond = function(line, _) - return not string.find(line, 'file:.*$') - end, - fetcher = function() - return vim.tbl_map(function(tag) - return ':' .. tag .. ':' - end, org.files:get_tags() or {}) - end, -} - -local todo_keywords = { - line_rgx = vim.regex([[^\*\+\s\+\w*$]]), - rgx = vim.regex([[\(^\(\*\+\s\+\)\?\)\@<=\w*$]]), - fetcher = function() - return config:get_todo_keywords().ALL - end, -} - -local contexts = { - directives, - begin_blocks, - filetags, - properties, - links, - metadata, -} - -local headline_contexts = { - tags, - links, - todo_keywords, -} - -local Omni = {} - ----@return string: the line before the current cursor position -function Omni.get_line_content_before_cursor() - return vim.api.nvim_get_current_line():sub(1, vim.api.nvim_call_function('col', { '.' }) - 1) -end - -function Omni.is_headline() - return Omni.get_line_content_before_cursor():match('^%*+%s+') -end - ----@return OrgTable -function Omni.get_all_contexts() - return Omni.is_headline() and headline_contexts or contexts -end - ----Determines an URL for link handling. Handles a couple of corner-cases ----@param base string The string to complete ----@return string -function Omni.get_url_str(line, base) - local line_base = line:match('%[%[(.-)$') or line - line_base = line_base:gsub(base .. '$', '') - return (line_base or '') .. (base or '') -end - ---- Is true and only true, if all given regex in the context match appropriatly ---- line_rgx and extra_cond are optional, but if the context defines them, they must match. ---- The basic rgx must always match the base, because it is used to determine the start position for ---- the completion. ----@param context table: the context candidate ----@param line string: characters left to the cursor ----@param base string: characters after the trigger (filter) -function Omni.all_ctx_conditions_apply(context, line, base) - return (not context.line_rgx or context.line_rgx:match_str(line)) - and context.rgx:match_str(base) - and (not context.extra_cond or context.extra_cond(line, base)) -end - ----@param base? string ----@return number -function Omni.find_start(base) - local line = Omni.get_line_content_before_cursor() - for _, context in ipairs(Omni.get_all_contexts()) do - local word = context.rgx:match_str(line) - if word and (not context.extra_cond or context.extra_cond(line, base)) then - return word - end - end - return -1 -end - ----@param base string ----@return table -function Omni.get_completions(base) - -- Workaround for the corner case of matching custom_ids to file paths without file: prefix - -- Bug is probably in the regex, but hard to fix, because the regex is so hard to read - base = base:match('^:#') and base:gsub('^:', '') or base - - local line = Omni.get_line_content_before_cursor() - local url = Url.new(Omni.get_url_str(line, base)) - local results = {} - for _, context in ipairs(Omni.get_all_contexts()) do - if Omni.all_ctx_conditions_apply(context, line, base) then - local items = {} - - -- fetch or just take context specific completion candidates - if context.fetcher then - items = context.fetcher(url) - else - items = { unpack(context.list) } - end - - -- incrementally limit candidates to what the user has already been typed - items = vim.tbl_filter(function(i) - return i:find('^' .. vim.pesc(base)) - end, items) - - -- craft the actual completion entries and append them to the overall results - for _, item in ipairs(items) do - table.insert(results, { word = item, menu = '[Org]' }) - end - end - end - - return results -end - -function Omni.omnifunc(findstart, base) - return findstart == 1 and Omni.find_start(base) or Omni.get_completions(base) -end - -return Omni diff --git a/lua/orgmode/org/autocompletion/sources/directives.lua b/lua/orgmode/org/autocompletion/sources/directives.lua new file mode 100644 index 000000000..8945f39f2 --- /dev/null +++ b/lua/orgmode/org/autocompletion/sources/directives.lua @@ -0,0 +1,40 @@ +---@class OrgCompletionDirectives:OrgCompletionSource +---@field private pattern vim.regex +local OrgCompletionDirectives = {} +OrgCompletionDirectives.__index = OrgCompletionDirectives + +function OrgCompletionDirectives:new() + return setmetatable({ + pattern = vim.regex([[^\s*\zs\#+\?\w*$]]), + }, OrgCompletionDirectives) +end + +---@param context OrgCompletionContext +---@return number | nil +function OrgCompletionDirectives:get_start(context) + return self.pattern:match_str(context.line) +end + +function OrgCompletionDirectives:get_name() + return 'directives' +end + +---@return string[] +function OrgCompletionDirectives:get_results(_) + return { + '#+title', + '#+author', + '#+email', + '#+name', + '#+filetags', + '#+archive', + '#+options', + '#+category', + '#+begin_src', + '#+begin_example', + '#+end_src', + '#+end_example', + } +end + +return OrgCompletionDirectives diff --git a/lua/orgmode/org/autocompletion/sources/hyperlinks.lua b/lua/orgmode/org/autocompletion/sources/hyperlinks.lua new file mode 100644 index 000000000..b2f51c110 --- /dev/null +++ b/lua/orgmode/org/autocompletion/sources/hyperlinks.lua @@ -0,0 +1,35 @@ +local Hyperlinks = require('orgmode.org.hyperlinks') +local Url = require('orgmode.objects.url') +local Link = require('orgmode.objects.link') +---@class OrgCompletionHyperlinks:OrgCompletionSource +---@field completion OrgCompletion +---@field private pattern vim.regex +local OrgCompletionHyperlinks = {} +OrgCompletionHyperlinks.__index = OrgCompletionHyperlinks + +---@param opts { completion: OrgCompletion } +function OrgCompletionHyperlinks:new(opts) + return setmetatable({ + completion = opts.completion, + pattern = vim.regex([[\s*\[\[\zs.*$]]), + }, OrgCompletionHyperlinks) +end + +function OrgCompletionHyperlinks:get_name() + return 'hyperlinks' +end + +---@param context OrgCompletionContext +---@return number | nil +function OrgCompletionHyperlinks:get_start(context) + return self.pattern:match_str(context.line) +end + +---@return string[] +function OrgCompletionHyperlinks:get_results(context) + local link = Link.new(context.base) + local result, mapper = Hyperlinks.find_matching_links(link.url) + return mapper(result) +end + +return OrgCompletionHyperlinks diff --git a/lua/orgmode/org/autocompletion/sources/plan.lua b/lua/orgmode/org/autocompletion/sources/plan.lua new file mode 100644 index 000000000..85fd09db9 --- /dev/null +++ b/lua/orgmode/org/autocompletion/sources/plan.lua @@ -0,0 +1,42 @@ +local config = require('orgmode.config') + +---@class OrgCompletionPlan:OrgCompletionSource +---@field completion OrgCompletion +---@field private pattern vim.regex +local OrgCompletionPlan = {} +OrgCompletionPlan.__index = OrgCompletionPlan + +---@param opts { completion: OrgCompletion } +function OrgCompletionPlan:new(opts) + local this = setmetatable({ + pattern = vim.regex([[\(^\s*\|\s\+\)\zs\w*$]]), + completion = opts.completion, + }, OrgCompletionPlan) + return this +end + +function OrgCompletionPlan:get_name() + return 'plan' +end + +---@param context OrgCompletionContext +---@return number | nil +function OrgCompletionPlan:get_start(context) + local prev_line = vim.fn.getline(vim.fn.line('.') - 1) + if not self.completion:is_headline_line(prev_line) then + return nil + end + + return self.pattern:match_str(context.line) +end + +---@return string[] +function OrgCompletionPlan:get_results(_) + return { + 'DEADLINE:', + 'SCHEDULED:', + 'CLOSED:', + } +end + +return OrgCompletionPlan diff --git a/lua/orgmode/org/autocompletion/sources/properties.lua b/lua/orgmode/org/autocompletion/sources/properties.lua new file mode 100644 index 000000000..ade57121c --- /dev/null +++ b/lua/orgmode/org/autocompletion/sources/properties.lua @@ -0,0 +1,42 @@ +---@class OrgCompletionProperties:OrgCompletionSource +---@field completion OrgCompletion +---@field private pattern vim.regex +local OrgCompletionProperties = {} +OrgCompletionProperties.__index = OrgCompletionProperties + +---@param opts { completion: OrgCompletion } +function OrgCompletionProperties:new(opts) + return setmetatable({ + completion = opts.completion, + pattern = vim.regex([[^\s*\zs:\w*$]]), + }, OrgCompletionProperties) +end + +function OrgCompletionProperties:get_name() + return 'properties' +end + +---@param context OrgCompletionContext +---@return number | nil +function OrgCompletionProperties:get_start(context) + if self.completion:is_headline_line(context.line) then + return nil + end + + return self.pattern:match_str(context.line) +end + +---@return string[] +function OrgCompletionProperties:get_results(_) + return { + ':PROPERTIES:', + ':END:', + ':LOGBOOK:', + ':STYLE:', + ':REPEAT_TO_STATE:', + ':CUSTOM_ID:', + ':CATEGORY:', + } +end + +return OrgCompletionProperties diff --git a/lua/orgmode/org/autocompletion/sources/tags.lua b/lua/orgmode/org/autocompletion/sources/tags.lua new file mode 100644 index 000000000..ed94c7e2f --- /dev/null +++ b/lua/orgmode/org/autocompletion/sources/tags.lua @@ -0,0 +1,37 @@ +---@class OrgCompletionTags:OrgCompletionSource +---@field completion OrgCompletion +---@field private pattern vim.regex +---@field private filetags_pattern vim.regex +local OrgCompletionTags = {} +OrgCompletionTags.__index = OrgCompletionTags + +---@param opts { completion: OrgCompletion } +function OrgCompletionTags:new(opts) + return setmetatable({ + completion = opts.completion, + filetags_pattern = vim.regex([[\c^\s*\#+filetags:\s\+]]), + pattern = vim.regex([[:\([0-9A-Za-z_%@\#]*\)$]]), + }, OrgCompletionTags) +end + +function OrgCompletionTags:get_name() + return 'tags' +end + +---@param context OrgCompletionContext +---@return number | nil +function OrgCompletionTags:get_start(context) + if not self.completion:is_headline_line(context.line) and not self.filetags_pattern:match_str(context.line) then + return nil + end + return self.pattern:match_str(context.line) +end + +---@return string[] +function OrgCompletionTags:get_results(_) + return vim.tbl_map(function(tag) + return table.concat({ ':', tag, ':' }, '') + end, self.completion.files:get_tags()) +end + +return OrgCompletionTags diff --git a/lua/orgmode/org/autocompletion/sources/todo_keywords.lua b/lua/orgmode/org/autocompletion/sources/todo_keywords.lua new file mode 100644 index 000000000..49e33f890 --- /dev/null +++ b/lua/orgmode/org/autocompletion/sources/todo_keywords.lua @@ -0,0 +1,30 @@ +local config = require('orgmode.config') + +---@class OrgCompletionTodoKeywords:OrgCompletionSource +---@field private pattern vim.regex +local OrgCompletionTodoKeywords = {} +OrgCompletionTodoKeywords.__index = OrgCompletionTodoKeywords + +function OrgCompletionTodoKeywords:new() + local this = setmetatable({ + pattern = vim.regex([[^\*\+\s\+\zs\w*$]]), + }, OrgCompletionTodoKeywords) + return this +end + +function OrgCompletionTodoKeywords:get_name() + return 'todo_keywords' +end + +---@param context OrgCompletionContext +---@return number | nil +function OrgCompletionTodoKeywords:get_start(context) + return self.pattern:match_str(context.line) +end + +---@return string[] +function OrgCompletionTodoKeywords:get_results(_) + return config:get_todo_keywords().ALL +end + +return OrgCompletionTodoKeywords diff --git a/lua/orgmode/org/hyperlinks.lua b/lua/orgmode/org/hyperlinks.lua index 209e3ef10..df5e71737 100644 --- a/lua/orgmode/org/hyperlinks.lua +++ b/lua/orgmode/org/hyperlinks.lua @@ -23,9 +23,9 @@ function Hyperlinks.find_by_filepath(url) return {} end --TODO integrate with orgmode.utils.fs or orgmode.objects.url - local file_base_no_start_path = file_base:gsub('^%./', '') .. '' + local file_base_no_start_path = vim.pesc(file_base:gsub('^%./', '') .. '') local is_relative_path = file_base:match('^%./') - local current_file_directory = fs.get_current_file_dir() + local current_file_directory = vim.pesc(fs.get_current_file_dir()) local valid_filenames = {} for _, f in ipairs(filenames) do if is_relative_path then @@ -40,10 +40,11 @@ function Hyperlinks.find_by_filepath(url) end end - -- Outer checks already filter cases where `ctx.skip_add_prefix` is truthy, - -- so no need to check it here + local protocol = url:get_protocol() + local prefix = protocol and protocol == 'file' and 'file:' or '' + return vim.tbl_map(function(path) - return 'file:' .. path + return prefix .. path end, valid_filenames) end @@ -59,26 +60,30 @@ function Hyperlinks.find_by_custom_id_property(url) return file:find_headlines_with_property_matching('CUSTOM_ID', custom_id) end ----@param headlines OrgHeadline[] ----@return string[] -function Hyperlinks.as_custom_id_anchors(headlines) - return vim.tbl_map(function(headline) - ---@cast headline OrgHeadline - local custom_id = headline:get_property('custom_id') - if custom_id then - return '#' .. custom_id - end - end, headlines) +---@param url OrgUrl +---@return fun(headlines: OrgHeadline[]): string[] +function Hyperlinks.as_custom_id_anchors(url) + local prefix = url:is_file_custom_id() and url:get_file() .. '::' or '' + return function(headlines) + return vim.tbl_map(function(headline) + ---@cast headline OrgHeadline + local custom_id = headline:get_property('custom_id') + return ('%s#%s'):format(prefix, custom_id) + end, headlines) + end end ----@param headlines OrgHeadline[] +---@param url OrgUrl ---@param omit_prefix? boolean ----@return string[] -function Hyperlinks.as_headline_anchors(headlines, omit_prefix) - return vim.tbl_map(function(headline) - local title = headline:get_title() - return omit_prefix and title or '*' .. title - end, headlines) +---@return fun(headlines: OrgHeadline[]): string[] +function Hyperlinks.as_headline_anchors(url, omit_prefix) + local prefix = url:is_file_headline() and url:get_file() .. '::' or '' + return function(headlines) + return vim.tbl_map(function(headline) + local title = (omit_prefix and '' or '*') .. headline:get_title() + return ('%s%s'):format(prefix, title) + end, headlines) + end end ---@param url OrgUrl @@ -101,11 +106,10 @@ end ---@return OrgHeadline[] function Hyperlinks.find_by_dedicated_target(url) local anchor = url and url:get_dedicated_target() - if anchor then - return org.files:get_current_file():find_headlines_matching_search_term(as_dedicated_anchor_pattern(anchor), true) - else + if not anchor then return {} end + return org.files:get_current_file():find_headlines_matching_search_term(as_dedicated_anchor_pattern(anchor), true) end ---@param url OrgUrl @@ -133,7 +137,7 @@ end function Hyperlinks.as_dedicated_anchors_or_internal_titles(url) return function(headlines) local dedicated_anchors = Hyperlinks.as_dedicated_targets(url)(headlines) - local fuzzy_titles = Hyperlinks.as_headline_anchors(headlines, true) + local fuzzy_titles = Hyperlinks.as_headline_anchors(url, true)(headlines) return utils.concat(dedicated_anchors, fuzzy_titles, true) end end @@ -151,10 +155,10 @@ function Hyperlinks.find_matching_links(url) result = Hyperlinks.find_by_filepath(url) elseif url:is_custom_id() then result = Hyperlinks.find_by_custom_id_property(url) - mapper = Hyperlinks.as_custom_id_anchors + mapper = Hyperlinks.as_custom_id_anchors(url) elseif url:is_headline() then result = Hyperlinks.find_by_title(url) - mapper = Hyperlinks.as_headline_anchors + mapper = Hyperlinks.as_headline_anchors(url) elseif url:is_dedicated_anchor_or_internal_title() then result = utils.concat( Hyperlinks.find_by_dedicated_target(url), @@ -199,18 +203,10 @@ function Hyperlinks.autocomplete_links(arg_lead) local url = Url.new(arg_lead) local result, mapper = Hyperlinks.find_matching_links(url) - if url:is_file_plain() then + if url:is_file_plain() or url:is_custom_id() or url:is_headline() then return mapper(result) end - if url:is_custom_id() or url:is_headline() then - local file = get_file_from_url(url) - local results = mapper(result) - return vim.tbl_map(function(value) - return ('file:%s::%s'):format(file.filename, value) - end, results) - end - return vim.tbl_keys(Hyperlinks.stored_links) end diff --git a/lua/orgmode/utils/fs.lua b/lua/orgmode/utils/fs.lua index c6d7d3726..cf2329e8d 100644 --- a/lua/orgmode/utils/fs.lua +++ b/lua/orgmode/utils/fs.lua @@ -36,7 +36,7 @@ end function M.get_current_file_dir() local current_file = utils.current_file_path() local current_dir = vim.fn.fnamemodify(current_file, ':p:h') - return current_dir + return current_dir or '' end return M diff --git a/queries/org/highlights.scm b/queries/org/highlights.scm index 57b3395c1..cb3eefcb6 100644 --- a/queries/org/highlights.scm +++ b/queries/org/highlights.scm @@ -28,6 +28,7 @@ (latex_env) @org.latex_env (drawer) @org.drawer (tag_list) @org.tag +(directive name: (expr) @_directive_name value: (value) @org.tag (#match? @_directive_name "\\c^filetags$")) (plan) @org.plan (comment) @org.comment @spell (directive) @org.directive diff --git a/tests/plenary/org/autocompletion_spec.lua b/tests/plenary/org/autocompletion_spec.lua index dd1ccd246..c04bc1556 100644 --- a/tests/plenary/org/autocompletion_spec.lua +++ b/tests/plenary/org/autocompletion_spec.lua @@ -1,160 +1,164 @@ -local Omni = require('orgmode.org.autocompletion.omni') local helpers = require('tests.plenary.helpers') +local org = require('orgmode') -describe('Autocompletion should properly find start offset for omni autocompletion', function() +describe('Autocompletion', function() local function setup_file(content) -- Add space to the end of content because insert mode in -- tests doesn't pick up a proper cursor location helpers.create_agenda_file({ content .. ' ' }) vim.cmd('norm!A') end - it('for an empty line', function() - setup_file('') - local result = Omni.find_start() - assert.are.same(0, result) - end) - it('for an empty headline', function() - setup_file('* ') - vim.cmd('norm!A') - local result = Omni.find_start() - assert.are.same(2, result) - end) - it('within TODO in headline', function() - setup_file('* TO') - vim.cmd('norm!A') - local result = Omni.find_start() - assert.are.same(2, result) - setup_file('* TODO') - vim.cmd('norm!A') - result = Omni.find_start() - assert.are.same(2, result) - end) - it('in the middle of a headline', function() - setup_file('* TODO some text ') - vim.cmd('norm!A') - local result = Omni.find_start() - assert.are.same(17, result) - end) - it('within tag in headline', function() - setup_file('* TODO tags goes at the end :') - local result = Omni.find_start() - assert.are.same(28, result) - - setup_file('* TODO tags goes at the end :SOMET') - result = Omni.find_start() - assert.are.same(28, result) - end) - it('after tag in headline', function() - setup_file('* TODO tags goes at the end :SOMETAG:') - local result = Omni.find_start() - assert.are.same(36, result) - end) - it('within special directives (#+)', function() - setup_file('#') - local result = Omni.find_start() - assert.are.same(0, result) - - setup_file('#+') - result = Omni.find_start() - assert.are.same(0, result) - - setup_file('#+ar') - result = Omni.find_start() - assert.are.same(0, result) - end) + describe('omni find start', function() + it('for an empty line', function() + setup_file('') + local result = org.completion:omnifunc(1) + assert.are.same(-1, result) + end) + it('for an empty headline', function() + setup_file('* ') + vim.cmd('norm!A') + local result = org.completion:omnifunc(1) + assert.are.same(2, result) + end) + it('within TODO in headline', function() + setup_file('* TO') + vim.cmd('norm!A') + local result = org.completion:omnifunc(1) + assert.are.same(2, result) + + setup_file('* TODO') + vim.cmd('norm!A') + result = org.completion:omnifunc(1) + assert.are.same(2, result) + end) + it('in the middle of a headline', function() + setup_file('* TODO some text ') + vim.cmd('norm!A') + local result = org.completion:omnifunc(1) + assert.are.same(-1, result) + end) + it('within tag in headline', function() + setup_file('* TODO tags goes at the end :') + local result = org.completion:omnifunc(1) + assert.are.same(28, result) + + setup_file('* TODO tags goes at the end :SOMET') + result = org.completion:omnifunc(1) + assert.are.same(28, result) + end) + it('after tag in headline', function() + setup_file('* TODO tags goes at the end :SOMETAG:') + local result = org.completion:omnifunc(1) + assert.are.same(36, result) + end) + it('within special directives (#+)', function() + setup_file('#') + local result = org.completion:omnifunc(1) + assert.are.same(0, result) + + setup_file('#+') + result = org.completion:omnifunc(1) + assert.are.same(0, result) + + setup_file('#+ar') + result = org.completion:omnifunc(1) + assert.are.same(0, result) + end) - describe('Autocompletion', function() it('within properties', function() setup_file(':') - local result = Omni.find_start() + local result = org.completion:omnifunc(1) assert.are.same(0, result) setup_file(' :') - result = Omni.find_start() + result = org.completion:omnifunc(1) assert.are.same(2, result) setup_file(' :PROP') - result = Omni.find_start() + result = org.completion:omnifunc(1) assert.are.same(2, result) setup_file(' :PROPERTI') - result = Omni.find_start() + result = org.completion:omnifunc(1) assert.are.same(2, result) end) it('within hyperlinks', function() setup_file(' [[') - local result = Omni.find_start() + local result = org.completion:omnifunc(1) assert.are.same(4, result) setup_file(' [[*some') - result = Omni.find_start() + result = org.completion:omnifunc(1) assert.are.same(4, result) setup_file(' [[#val') - result = Omni.find_start() + result = org.completion:omnifunc(1) assert.are.same(4, result) setup_file(' [[test') - result = Omni.find_start() + result = org.completion:omnifunc(1) assert.are.same(4, result) setup_file(' [[file:') - result = Omni.find_start() + result = org.completion:omnifunc(1) assert.are.same(4, result) end) it('within file hyperlink anchors (file: prefix)', function() setup_file(' [[file:./some/path/file.org::*') - local result = Omni.find_start() - assert.are.same(31, result) + local result = org.completion:omnifunc(1) + assert.are.same(4, result) setup_file(' [[file:./some/path/file.org::#') - result = Omni.find_start() - assert.are.same(31, result) + result = org.completion:omnifunc(1) + assert.are.same(4, result) setup_file(' [[file:./some/path/file.org::') - result = Omni.find_start() - assert.are.same(31, result) + result = org.completion:omnifunc(1) + assert.are.same(4, result) end) it('within file hyperlink anchors (./ prefix, headline)', function() setup_file(' [[./1-34_some/path/file.org::*') - local result = Omni.find_start() - assert.are.same(31, result) + local result = org.completion:omnifunc(1) + assert.are.same(4, result) end) --TODO These tests expose a bug. Actually the expected start should be 31 as in the tests before it('within file hyperlink anchors (./ prefix, custom_id)', function() setup_file(' [[./1-34_some/path/file.org::#') - local result = Omni.find_start() - assert.are.same(30, result) + local result = org.completion:omnifunc(1) + assert.are.same(4, result) end) it('within file hyperlink anchors (./ prefix, dedicated anchor)', function() setup_file(' [[./1-34_some/path/file.org::') - local result = Omni.find_start() - assert.are.same(30, result) + local result = org.completion:omnifunc(1) + assert.are.same(4, result) end) end) - describe('Autocompletion', function() - before_each(function() - setup_file('') - end) - + describe('omni complete', function() it('should return an empty table when base is empty', function() setup_file('') - local result = Omni.get_completions('') + local result = org.completion:omnifunc(0, '') assert.are.same({}, result) end) - it('should return DEADLINE: when base is D', function() + it('should return DEADLINE: when base is D on second headline line', function() -- Metadata - local result = Omni.get_completions('D') + helpers.create_agenda_file({ + '* TODO test', + ' A', + }) + vim.fn.cursor({ 2, 1 }) + vim.cmd('norm!A') + local result = org.completion:omnifunc(0, 'D') assert.are.same({ { menu = '[Org]', word = 'DEADLINE:' }, }, result) end) it('should return defined keywords when base is :', function() - local result = Omni.get_completions(':') + setup_file(':') + local result = org.completion:omnifunc(0, ':') local props = { { menu = '[Org]', word = ':PROPERTIES:' }, { menu = '[Org]', word = ':END:' }, @@ -168,21 +172,23 @@ describe('Autocompletion should properly find start offset for omni autocompleti end) it('should filter keywords down', function() - local result = Omni.get_completions(':C') + setup_file(':') + local result = org.completion:omnifunc(0, ':C') assert.are.same({ { menu = '[Org]', word = ':CUSTOM_ID:' }, { menu = '[Org]', word = ':CATEGORY:' }, }, result) - result = Omni.get_completions(':CA') + result = org.completion:omnifunc(0, ':CA') assert.are.same({ { menu = '[Org]', word = ':CATEGORY:' }, }, result) end) it('should find and filter down export options when base is #', function() + setup_file('#') -- Directives - local result = Omni.get_completions('#') + local result = org.completion:omnifunc(0, '#') local directives = { { menu = '[Org]', word = '#+title' }, { menu = '[Org]', word = '#+author' }, @@ -193,149 +199,146 @@ describe('Autocompletion should properly find start offset for omni autocompleti { menu = '[Org]', word = '#+options' }, { menu = '[Org]', word = '#+category' }, { menu = '[Org]', word = '#+begin_src' }, - { menu = '[Org]', word = '#+end_src' }, { menu = '[Org]', word = '#+begin_example' }, + { menu = '[Org]', word = '#+end_src' }, { menu = '[Org]', word = '#+end_example' }, } assert.are.same(directives, result) - result = Omni.get_completions('#+') + result = org.completion:omnifunc(0, '#+') assert.are.same(directives, result) - result = Omni.get_completions('#+b') + result = org.completion:omnifunc(0, '#+b') assert.are.same({ { menu = '[Org]', word = '#+begin_src' }, { menu = '[Org]', word = '#+begin_example' }, }, result) end) - end) - before_each(function() - setup_file('* ') - end) + it('should find and filter down TODO keywords at the beginning of a headline', function() + setup_file('* ') + local result = org.completion:omnifunc(0, '') + assert.are.same({ + { menu = '[Org]', word = 'TODO' }, + { menu = '[Org]', word = 'DONE' }, + }, result) - it('should find and filter down TODO keywords at the beginning of a headline', function() - local result = Omni.get_completions('') - assert.are.same({ - { menu = '[Org]', word = 'TODO' }, - { menu = '[Org]', word = 'DONE' }, - }, result) - - setup_file('* T') - result = Omni.get_completions('T') - assert.are.same({ - { menu = '[Org]', word = 'TODO' }, - }, result) - end) + setup_file('* T') + result = org.completion:omnifunc(0, 'T') + assert.are.same({ + { menu = '[Org]', word = 'TODO' }, + }, result) + end) - it('should find defined tags', function() - local file = helpers.create_agenda_file({ - '#+filetags: :OFFICE:PRIVATE:', - }) - setup_file('* TODO tags go at the end :') - local result = Omni.get_completions(':') - assert.are.same({ - { menu = '[Org]', word = ':OFFICE:' }, - { menu = '[Org]', word = ':PRIVATE:' }, - { menu = '[Org]', word = ':SOMETAG:' }, - }, result) - - result = Omni.get_completions(':OFF') - assert.are.same({ - { menu = '[Org]', word = ':OFFICE:' }, - }, result) - - vim.fn.setline(1, '* TODO tags go at the end :OFFICE:') - result = Omni.get_completions(':') - assert.are.same({ - { menu = '[Org]', word = ':OFFICE:' }, - { menu = '[Org]', word = ':PRIVATE:' }, - { menu = '[Org]', word = ':SOMETAG:' }, - }, result) - - setup_file('#+filetags: ') - result = Omni.get_completions('') - assert.are.same({}, result) - -- - setup_file('#+filetags: :') - result = Omni.get_completions(':') - assert.are.same({ - { menu = '[Org]', word = ':OFFICE:' }, - { menu = '[Org]', word = ':PRIVATE:' }, - { menu = '[Org]', word = ':SOMETAG:' }, - }, result) - end) -end) + it('should find defined tags', function() + local file = helpers.create_agenda_file({ + '#+filetags: :OFFICE:PRIVATE:', + }) + setup_file('* TODO tags go at the end :') + local result = org.completion:omnifunc(0, ':') + assert.are.same({ + { menu = '[Org]', word = ':OFFICE:' }, + { menu = '[Org]', word = ':PRIVATE:' }, + { menu = '[Org]', word = ':SOMETAG:' }, + }, result) -describe('Autocompletion in hyperlinks', function() - it('should complete headlines', function() - local orgfile = helpers.create_agenda_file({ - '* Item for work 1', - '* Item for work 2', - }) - local filename = vim.fn.fnamemodify(orgfile.filename, ':t') - local file_path_relative = string.format('./%s', filename) - - local line = string.format(' [[%s::* ', file_path_relative) - helpers.create_file({ line }) - - vim.fn.cursor({ 1, #line }) - local result = Omni.get_completions('') - assert.are.same({ - { menu = '[Org]', word = '*Item for work 1' }, - { menu = '[Org]', word = '*Item for work 2' }, - }, result) - end) + result = org.completion:omnifunc(0, ':OFF') + assert.are.same({ + { menu = '[Org]', word = ':OFFICE:' }, + }, result) - it('should complete custom_ids', function() - local orgfile = helpers.create_agenda_file({ - '* Item for work 1', - ':PROPERTIES:', - ':CUSTOM_ID: ID_1', - ':END:', - '* Item for work 2', - ':PROPERTIES:', - ':CUSTOM_ID: ID_2', - ':END:', - }) - local filename = vim.fn.fnamemodify(orgfile.filename, ':t') - local file_path_relative = string.format('./%s', filename) - - local line = string.format(' [[%s::# ', file_path_relative) - helpers.create_file({ line }) - - vim.fn.cursor({ 1, #line }) - local result = Omni.get_completions('') - assert.are.same({ - { menu = '[Org]', word = '#ID_1' }, - { menu = '[Org]', word = '#ID_2' }, - }, result) - end) + vim.fn.setline(1, '* TODO tags go at the end :OFFICE:') + result = org.completion:omnifunc(0, ':') + assert.are.same({ + { menu = '[Org]', word = ':OFFICE:' }, + { menu = '[Org]', word = ':PRIVATE:' }, + { menu = '[Org]', word = ':SOMETAG:' }, + }, result) + + setup_file('#+filetags: ') + result = org.completion:omnifunc(0, '') + assert.are.same({}, result) + -- + setup_file('#+filetags: :') + result = org.completion:omnifunc(0, ':') + assert.are.same({ + { menu = '[Org]', word = ':OFFICE:' }, + { menu = '[Org]', word = ':PRIVATE:' }, + { menu = '[Org]', word = ':SOMETAG:' }, + }, result) + end) - it('should complete fuzzy titles', function() - helpers.create_agenda_file({ - '* Title with an <>', - 'line1', - 'line2', - 'line3', - '* This headline should not be found', - 'line1', - '... <> ...', - 'line3', - '* Title without anchor', - 'line1', - 'line2', - 'line3', - '', - ' [[Tit ', - }) - vim.fn.cursor({ 14, 8 }) - - local result = Omni.get_completions('Tit') - - assert.are.same({ - { menu = '[Org]', word = 'Title with an <>' }, - { menu = '[Org]', word = 'Title without anchor' }, - }, result) + describe('in hyperlinks', function() + it('should complete headlines', function() + local orgfile = helpers.create_agenda_file({ + '* Item for work 1', + '* Item for work 2', + }) + local filename = vim.fn.fnamemodify(orgfile.filename, ':t') + local file_path_relative = string.format('./%s', filename) + + local line = string.format(' [[%s::* ', file_path_relative) + helpers.create_file({ line }) + + vim.fn.cursor({ 1, #line }) + local result = org.completion:omnifunc(0, ('%s::*'):format(file_path_relative)) + assert.are.same({ + { menu = '[Org]', word = ('%s::*Item for work 1'):format(file_path_relative) }, + { menu = '[Org]', word = ('%s::*Item for work 2'):format(file_path_relative) }, + }, result) + end) + + it('should complete custom_ids', function() + local orgfile = helpers.create_agenda_file({ + '* Item for work 1', + ':PROPERTIES:', + ':CUSTOM_ID: ID_1', + ':END:', + '* Item for work 2', + ':PROPERTIES:', + ':CUSTOM_ID: ID_2', + ':END:', + }) + local filename = vim.fn.fnamemodify(orgfile.filename, ':t') + local file_path_relative = string.format('./%s', filename) + + local line = string.format(' [[%s::# ', file_path_relative) + helpers.create_file({ line }) + + vim.fn.cursor({ 1, #line }) + local result = org.completion:omnifunc(0, ('%s::#'):format(file_path_relative)) + assert.are.same({ + { menu = '[Org]', word = ('%s::#ID_1'):format(file_path_relative) }, + { menu = '[Org]', word = ('%s::#ID_2'):format(file_path_relative) }, + }, result) + end) + + it('should complete fuzzy titles', function() + helpers.create_agenda_file({ + '* Title with an <>', + 'line1', + 'line2', + 'line3', + '* This headline should not be found', + 'line1', + '... <> ...', + 'line3', + '* Title without anchor', + 'line1', + 'line2', + 'line3', + '', + ' [[Tit ', + }) + vim.fn.cursor({ 14, 8 }) + + local result = org.completion:omnifunc(0, 'Tit') + + assert.are.same({ + { menu = '[Org]', word = 'Title with an <>' }, + { menu = '[Org]', word = 'Title without anchor' }, + }, result) + end) + end) end) end)