Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/omnicompletion #620

Merged
merged 6 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ftplugin/org.vim
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function! OrgmodeFoldText()
endfunction

function! OrgmodeOmni(findstart, base)
return luaeval('require("orgmode.org.autocompletion.omni")(_A[1], _A[2])', [a:findstart, a:base])
return luaeval('require("orgmode.org.autocompletion.omni").omnifunc(_A[1], _A[2])', [a:findstart, a:base])
endfunction

function! OrgmodeFormatExpr()
Expand Down
6 changes: 3 additions & 3 deletions lua/orgmode/org/autocompletion/cmp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ if not has_cmp then
return
end

local OrgmodeOmniCompletion = require('orgmode.org.autocompletion.omni')
local Omni = require('orgmode.org.autocompletion.omni')

local Source = {}

Expand All @@ -25,9 +25,9 @@ function Source:get_trigger_characters(_)
end

function Source:complete(params, callback)
local offset = OrgmodeOmniCompletion(1, '') + 1
local offset = Omni.find_start() + 1
local input = string.sub(params.context.cursor_before_line, offset)
local results = OrgmodeOmniCompletion(0, input)
local results = Omni.get_completions(input)
local items = {}
for _, item in ipairs(results) do
table.insert(items, {
Expand Down
6 changes: 3 additions & 3 deletions lua/orgmode/org/autocompletion/compe.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ if not has_compe then
return
end

local OrgmodeOmniCompletion = require('orgmode.org.autocompletion.omni')
local Omni = require('orgmode.org.autocompletion.omni')

local CompeSource = {}

Expand All @@ -22,7 +22,7 @@ function CompeSource.get_metadata()
end

function CompeSource.determine(_, context)
local offset = OrgmodeOmniCompletion(1, '') + 1
local offset = Omni.find_start() + 1
if offset > 0 then
return {
keyword_pattern_offset = offset,
Expand All @@ -32,7 +32,7 @@ function CompeSource.determine(_, context)
end

function CompeSource.complete(_, context)
local items = OrgmodeOmniCompletion(0, context.input)
local items = Omni.get_completions(context.input)
context.callback({
items = items,
incomplete = true,
Expand Down
91 changes: 62 additions & 29 deletions lua/orgmode/org/autocompletion/omni.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ local Files = require('orgmode.parser.files')
local config = require('orgmode.config')
local Hyperlinks = require('orgmode.org.hyperlinks')
local Url = require('orgmode.objects.url')
local Link = require('orgmode.objects.link')

local data = {
directives = { '#+title', '#+author', '#+email', '#+name', '#+filetags', '#+archive', '#+options', '#+category' },
Expand Down Expand Up @@ -33,8 +32,8 @@ local properties = {
}

local links = {
line_rgx = vim.regex([[\(\(^\|\s\+\)\[\[\)\@<=\(\*\|\#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\|_\|\d\)\+\)\?]]),
rgx = vim.regex([[\(\*\|\#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\|_\|\d\)\+\)\?$]]),
line_rgx = vim.regex([[\(\(^\|\s\+\)\[\[\)\@<=\(\*\|#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\)\+\)\?]]),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this regex changed?

Copy link
Contributor Author

@seflue seflue Oct 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hunting a bug (for which I currently only have a workaround), which I suspect to be in the regexp. Because of that I took a deeper dive into the regexp documentation and found out, that it was not the minimal regexp for what it tries to match. Some escapes are unnecessary and the \d and underscore were redundant, because they are already captured by \w. I tried to explain both simplifications in the corresponding commit message. Actually I always try to be as explicit as possible in my commits, so reading my commit messages can be very helpful when reviewing my PRs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification!

rgx = vim.regex([[\(\*\|#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\)\+\)\?$]]),
fetcher = function(url)
local hyperlinks, mapper = Hyperlinks.find_matching_links(url)
return mapper(hyperlinks)
Expand Down Expand Up @@ -91,54 +90,84 @@ local headline_contexts = {
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 Table
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
local function get_url_str(line, base)
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

--- This function is registered to omnicompletion in ftplugin/org.vim.
---
--- If the user want to use it in his completion plugin (like cmp) he has to do
--- that in the configuration of that plugin.
---@return table
local function omni(findstart, base)
local line = vim.api.nvim_get_current_line():sub(1, vim.api.nvim_call_function('col', { '.' }) - 1)
local is_headline = line:match('^%*+%s+')
local ctx = is_headline and headline_contexts or contexts
if findstart == 1 then
for _, context in ipairs(ctx) 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
--- 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
return -1
end
return -1
end

local url = Url.new(get_url_str(line, base))
local results = {}
---@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

for _, context in ipairs(ctx) do
if
(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))
then
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
Expand All @@ -148,4 +177,8 @@ local function omni(findstart, base)
return results
end

return omni
function Omni.omnifunc(findstart, base)
return findstart == 1 and Omni.find_start(base) or Omni.get_completions(base)
end

return Omni
Loading