mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-03 05:50:05 +08:00
228 lines
6.6 KiB
Lua
228 lines
6.6 KiB
Lua
local cmp = require('cmp')
|
|
|
|
---@param patterns string[]
|
|
---@param head boolean
|
|
---@return table #regex object
|
|
local function create_regex(patterns, head)
|
|
local pattern = [[\%(]] .. table.concat(patterns, [[\|]]) .. [[\)]]
|
|
if head then
|
|
pattern = '^' .. pattern
|
|
end
|
|
return vim.regex(pattern)
|
|
end
|
|
|
|
local DEFAULT_OPTION = {
|
|
ignore_cmds = { 'Man', '!' }
|
|
}
|
|
|
|
local MODIFIER_REGEX = create_regex({
|
|
[=[\s*abo\%[veleft]\s*]=],
|
|
[=[\s*bel\%[owright]\s*]=],
|
|
[=[\s*bo\%[tright]\s*]=],
|
|
[=[\s*bro\%[wse]\s*]=],
|
|
[=[\s*conf\%[irm]\s*]=],
|
|
[=[\s*hid\%[e]\s*]=],
|
|
[=[\s*keepal\s*t]=],
|
|
[=[\s*keeppa\%[tterns]\s*]=],
|
|
[=[\s*lefta\%[bove]\s*]=],
|
|
[=[\s*loc\%[kmarks]\s*]=],
|
|
[=[\s*nos\%[wapfile]\s*]=],
|
|
[=[\s*rightb\%[elow]\s*]=],
|
|
[=[\s*sil\%[ent]\s*]=],
|
|
[=[\s*tab\s*]=],
|
|
[=[\s*to\%[pleft]\s*]=],
|
|
[=[\s*verb\%[ose]\s*]=],
|
|
[=[\s*vert\%[ical]\s*]=],
|
|
}, true)
|
|
|
|
local COUNT_RANGE_REGEX = create_regex({
|
|
[=[\s*\%(\d\+\|\$\)\%[,\%(\d\+\|\$\)]\s*]=],
|
|
[=[\s*'\%[<,'>]\s*]=],
|
|
[=[\s*\%(\d\+\|\$\)\s*]=],
|
|
}, true)
|
|
|
|
local ONLY_RANGE_REGEX = create_regex({
|
|
[=[^\s*\%(\d\+\|\$\)\%[,\%(\d\+\|\$\)]\s*$]=],
|
|
[=[^\s*'\%[<,'>]\s*$]=],
|
|
[=[^\s*\%(\d\+\|\$\)\s*$]=],
|
|
}, true)
|
|
|
|
local OPTION_NAME_COMPLETION_REGEX = create_regex({
|
|
[=[se\%[tlocal][^=]*$]=],
|
|
}, true)
|
|
|
|
---@param word string
|
|
---@return boolean?
|
|
local function is_boolean_option(word)
|
|
local ok, opt = pcall(function()
|
|
return vim.opt[word]:get()
|
|
end)
|
|
if ok then
|
|
return type(opt) == 'boolean'
|
|
end
|
|
end
|
|
|
|
---@class cmp.Cmdline.Definition
|
|
---@field ctype string
|
|
---@field regex string
|
|
---@field kind lsp.CompletionItemKind
|
|
---@field isIncomplete boolean
|
|
---@field exec fun(option: table, arglead: string, cmdline: string, force: boolean): lsp.CompletionItem[]
|
|
---@field fallback boolean?
|
|
|
|
---@type cmp.Cmdline.Definition[]
|
|
local definitions = {
|
|
{
|
|
ctype = 'cmdline',
|
|
regex = [=[[^[:blank:]]*$]=],
|
|
kind = cmp.lsp.CompletionItemKind.Variable,
|
|
isIncomplete = true,
|
|
exec = function(option, arglead, cmdline, force)
|
|
-- Ignore range only cmdline. (e.g.: 4, '<,'>)
|
|
if not force and ONLY_RANGE_REGEX:match_str(cmdline) then
|
|
return {}
|
|
end
|
|
|
|
local _, parsed = pcall(function()
|
|
local target = cmdline
|
|
local s, e = COUNT_RANGE_REGEX:match_str(target)
|
|
if s and e then
|
|
target = target:sub(e + 1)
|
|
end
|
|
-- nvim_parse_cmd throw error when the cmdline contains range specifier.
|
|
return vim.api.nvim_parse_cmd(target, {}) or {}
|
|
end)
|
|
parsed = parsed or {}
|
|
|
|
-- Check ignore cmd.
|
|
if vim.tbl_contains(option.ignore_cmds, parsed.cmd) then
|
|
return {}
|
|
end
|
|
|
|
-- Cleanup modifiers.
|
|
-- We can just remove modifiers because modifiers is always separated by space.
|
|
if arglead ~= cmdline then
|
|
while true do
|
|
local s, e = MODIFIER_REGEX:match_str(cmdline)
|
|
if s == nil then
|
|
break
|
|
end
|
|
cmdline = string.sub(cmdline, e + 1)
|
|
end
|
|
end
|
|
|
|
-- Support `lua vim.treesitter._get|` or `'<,'>del|` completion.
|
|
-- In this case, the `vim.fn.getcompletion` will return only `get_query` for `vim.treesitter.get_|`.
|
|
-- We should detect `vim.treesitter.` and `get_query` separately.
|
|
-- TODO: The `\h\w*` was choosed by huristic. We should consider more suitable detection.
|
|
local fixed_input
|
|
do
|
|
local suffix_pos = vim.regex([[\h\w*$]]):match_str(arglead)
|
|
fixed_input = string.sub(arglead, 1, suffix_pos or #arglead)
|
|
end
|
|
|
|
-- The `vim.fn.getcompletion` does not return `*no*cursorline` option.
|
|
-- cmp-cmdline corrects `no` prefix for option name.
|
|
local is_option_name_completion = OPTION_NAME_COMPLETION_REGEX:match_str(cmdline) ~= nil
|
|
|
|
local items = {}
|
|
local escaped = cmdline:gsub([[\\]], [[\\\\]]);
|
|
for _, word_or_item in ipairs(vim.fn.getcompletion(escaped, 'cmdline')) do
|
|
local word = type(word_or_item) == 'string' and word_or_item or word_or_item.word
|
|
local item = { label = word }
|
|
table.insert(items, item)
|
|
if is_option_name_completion and is_boolean_option(word) then
|
|
table.insert(items, vim.tbl_deep_extend('force', {}, item, {
|
|
label = 'no' .. word,
|
|
filterText = word,
|
|
}))
|
|
end
|
|
end
|
|
for _, item in ipairs(items) do
|
|
if not string.find(item.label, fixed_input, 1, true) then
|
|
item.label = fixed_input .. item.label
|
|
end
|
|
end
|
|
return items
|
|
end
|
|
},
|
|
}
|
|
|
|
local source = {}
|
|
|
|
source.new = function()
|
|
return setmetatable({
|
|
before_line = '',
|
|
offset = -1,
|
|
ctype = '',
|
|
items = {},
|
|
}, { __index = source })
|
|
end
|
|
|
|
source.get_keyword_pattern = function()
|
|
return [=[[^[:blank:]]*]=]
|
|
end
|
|
|
|
source.get_trigger_characters = function()
|
|
return { ' ', '.', '#', '-' }
|
|
end
|
|
|
|
source.complete = function(self, params, callback)
|
|
local offset = 0
|
|
local ctype = ''
|
|
local items = {}
|
|
local kind
|
|
local isIncomplete = false
|
|
for _, def in ipairs(definitions) do
|
|
local s, e = vim.regex(def.regex):match_str(params.context.cursor_before_line)
|
|
if s and e then
|
|
offset = s
|
|
ctype = def.ctype
|
|
items = def.exec(
|
|
vim.tbl_deep_extend('keep', params.option or {}, DEFAULT_OPTION),
|
|
string.sub(params.context.cursor_before_line, s + 1),
|
|
params.context.cursor_before_line,
|
|
params.context:get_reason() == cmp.ContextReason.Manual
|
|
)
|
|
kind = def.kind
|
|
isIncomplete = def.isIncomplete
|
|
if not (#items == 0 and def.fallback) then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
local labels = {}
|
|
for _, item in ipairs(items) do
|
|
item.kind = kind
|
|
labels[item.label] = true
|
|
end
|
|
|
|
-- `vim.fn.getcompletion` does not handle fuzzy matches. So, we must return all items, including items that were matched in the previous input.
|
|
local should_merge_previous_items = false
|
|
if #params.context.cursor_before_line > #self.before_line then
|
|
should_merge_previous_items = string.find(params.context.cursor_before_line, self.before_line, 1, true) == 1
|
|
elseif #params.context.cursor_before_line < #self.before_line then
|
|
should_merge_previous_items = string.find(self.before_line, params.context.cursor_before_line, 1, true) == 1
|
|
end
|
|
|
|
if should_merge_previous_items and self.offset == offset and self.ctype == ctype then
|
|
for _, item in ipairs(self.items) do
|
|
if not labels[item.label] then
|
|
table.insert(items, item)
|
|
end
|
|
end
|
|
end
|
|
self.before_line = params.context.cursor_before_line
|
|
self.offset = offset
|
|
self.ctype = ctype
|
|
self.items = items
|
|
|
|
callback({
|
|
isIncomplete = isIncomplete,
|
|
items = items,
|
|
})
|
|
end
|
|
|
|
return source
|