1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-03 00:50:05 +08:00
SpaceVim/bundle/cmp-cmdline/lua/cmp_cmdline/init.lua
2023-06-09 12:10:07 +08:00

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