M lua/complementree/combinators.lua => lua/complementree/combinators.lua +57 -57
@@ 1,87 1,87 @@
local M = {}
local function complete(col, matches)
- if matches and #matches > 0 then
- vim.fn.complete(col, matches)
- return true
- else
- return false
- end
+ if matches and #matches > 0 then
+ vim.fn.complete(col, matches)
+ return true
+ else
+ return false
+ end
end
function M.combine(...)
- local funcs = { ... }
- return function(ltc, lnum)
- local matches = {}
- local coherent_p
- for _, f in ipairs(funcs) do
- local m, p = f(ltc, lnum)
- if not coherent_p then
- coherent_p = p
- end
+ local funcs = { ... }
+ return function(ltc, lnum)
+ local matches = {}
+ local coherent_p
+ for _, f in ipairs(funcs) do
+ local m, p = f(ltc, lnum)
+ if not coherent_p then
+ coherent_p = p
+ end
- if coherent_p == p then
- vim.list_extend(matches, m)
- end
+ if coherent_p == p then
+ vim.list_extend(matches, m)
end
- return matches, coherent_p
- end
+ end
+ return matches, coherent_p
+ end
end
function M.optional(mandat, opt)
- return function(ltc, lnum)
- local matches, prefix = mandat(ltc, lnum)
- if #matches > 0 then
- local m, p = opt(ltc, lnum)
- if p == prefix then
- vim.list_extend(matches, m)
- end
- return matches, prefix
- else
- return {}, ''
+ return function(ltc, lnum)
+ local matches, prefix = mandat(ltc, lnum)
+ if #matches > 0 then
+ local m, p = opt(ltc, lnum)
+ if p == prefix then
+ vim.list_extend(matches, m)
end
- end
+ return matches, prefix
+ else
+ return {}, ''
+ end
+ end
end
function M.non_empty_prefix(func)
- return function(ltc, lnum)
- local compl, prefix = func(ltc, lnum)
- if #prefix > 1 then
- return complete(#ltc - #prefix + 1, compl)
- else
- return false
- end
- end
+ return function(ltc, lnum)
+ local compl, prefix = func(ltc, lnum)
+ if #prefix > 1 then
+ return complete(#ltc - #prefix + 1, compl)
+ else
+ return false
+ end
+ end
end
function M.wrap(func)
- return function(ltc, lnum)
- local compl, prefix = func(ltc, lnum)
- return complete(#ltc - #prefix + 1, compl)
- end
+ return function(ltc, lnum)
+ local compl, prefix = func(ltc, lnum)
+ return complete(#ltc - #prefix + 1, compl)
+ end
end
function M.pipeline(source, ...)
- local current = source
- for _, func in ipairs({ ... }) do
- current = func(current)
- end
+ local current = source
+ for _, func in ipairs({ ... }) do
+ current = func(current)
+ end
- return M.wrap(current)
+ return M.wrap(current)
end
function M.chain(...)
- local funcs = { ... }
- return function(ltc, lnum)
- for _, f in ipairs(funcs) do
- local c, pref = f(ltc, lnum)
- if #c > 0 then
- return c, pref
- end
+ local funcs = { ... }
+ return function(ltc, lnum)
+ for _, f in ipairs(funcs) do
+ local c, pref = f(ltc, lnum)
+ if #c > 0 then
+ return c, pref
end
- return {}, ''
- end
+ end
+ return {}, ''
+ end
end
return M
M lua/complementree/comparators.lua => lua/complementree/comparators.lua +37 -37
@@ 10,56 10,56 @@ local Comparators = {}
local function mk_comparator(func)
- return function(msource)
- return function(ltc, lnum)
- local orig, prefix = msource(ltc, lnum)
- local cmp_cache = {}
- table.sort(orig, function(a, b)
- local key = { a, b }
+ return function(msource)
+ return function(ltc, lnum)
+ local orig, prefix = msource(ltc, lnum)
+ local cmp_cache = {}
+ table.sort(orig, function(a, b)
+ local key = { a, b }
- if not cmp_cache[key] then
- cmp_cache[key] = func(utils.cword(a), utils.cword(b))
- end
+ if not cmp_cache[key] then
+ cmp_cache[key] = func(utils.cword(a), utils.cword(b))
+ end
- return cmp_cache[key]
- end)
- return orig, prefix
- end
- end
+ return cmp_cache[key]
+ end)
+ return orig, prefix
+ end
+ end
end
Comparators.alphabetic = mk_comparator(function(a, b)
- return a < b
+ return a < b
end)
Comparators.length = mk_comparator(function(a, b)
- return #a < #b
+ return #a < #b
end)
local ok_fzy, fzy = pcall(require, 'fzy')
if ok_fzy then
- function Comparators.fzy(msource)
- return function(ltc, lnum)
- local orig, prefix = msource(ltc, lnum)
- local scores = {}
- local matching = {}
- if prefix ~= "" then
- for _, a in ipairs(orig) do
- local s, _ = fzy.match(prefix, utils.cword(a))
- if math.abs(s or math.huge) ~= math.huge or prefix == utils.cword(a) then
- scores[a] = s
- table.insert(matching, a)
- end
- end
- table.sort(matching, function(a, b)
- return scores[a] > scores[b]
- end)
- return matching, prefix
- else
- return orig, prefix
- end
+ function Comparators.fzy(msource)
+ return function(ltc, lnum)
+ local orig, prefix = msource(ltc, lnum)
+ local scores = {}
+ local matching = {}
+ if prefix ~= "" then
+ for _, a in ipairs(orig) do
+ local s, _ = fzy.match(prefix, utils.cword(a))
+ if math.abs(s or math.huge) ~= math.huge or prefix == utils.cword(a) then
+ scores[a] = s
+ table.insert(matching, a)
+ end
+ end
+ table.sort(matching, function(a, b)
+ return scores[a] > scores[b]
+ end)
+ return matching, prefix
+ else
+ return orig, prefix
end
- end
+ end
+ end
end
return Comparators
M lua/complementree/defaults.lua => lua/complementree/defaults.lua +7 -13
@@ 1,13 1,5 @@
local Defaults = {}
-
-
-
-
-
-
-
-
local comb = require('complementree.combinators')
local sources = require('complementree.sources')
local filters = require('complementree.filters')
@@ 15,17 7,19 @@ local comp = require('complementree.comparators')
local utils = require('complementree.utils')
function Defaults.ins_completion(mode)
- return function()
- utils.feed(string.format('<C-X><%s>', mode))
- return vim.fn.pumvisible() == 1
- end
+ return function()
+ utils.feed(string.format('<C-X><%s>', mode))
+ return vim.fn.pumvisible() == 1
+ end
end
function Defaults.dummy(_, _)
end
-Defaults.luasnip = comb.pipeline(sources.luasnip_matches({}), filters.prefix, comp.alphabetic)
+if sources.luasnip_matches then
+ Defaults.luasnip = comb.pipeline(sources.luasnip_matches({}), filters.prefix, comp.alphabetic)
+end
Defaults.lsp = comb.pipeline(sources.lsp_matches({}), filters.prefix, comp.alphabetic)
M lua/complementree/filters.lua => lua/complementree/filters.lua +20 -20
@@ 11,39 11,39 @@ local Filters = {}
local function mk_filter(func)
- return function(msource)
- return function(ltc, lnum)
- local orig, prefix = msource(ltc, lnum)
- local filtered = {}
- for i, v in ipairs(orig) do
- if func(i, v, prefix) then
- table.insert(filtered, v)
- end
- end
- return filtered, prefix
+ return function(msource)
+ return function(ltc, lnum)
+ local orig, prefix = msource(ltc, lnum)
+ local filtered = {}
+ for i, v in ipairs(orig) do
+ if func(i, v, prefix) then
+ table.insert(filtered, v)
+ end
end
- end
+ return filtered, prefix
+ end
+ end
end
function Filters.amount(n)
- return mk_filter(function(i, _, _)
- return i <= n
- end)
+ return mk_filter(function(i, _, _)
+ return i <= n
+ end)
end
Filters.prefix = mk_filter(function(_, v, prefix)
- return vim.startswith(utils.cword(v), prefix)
+ return vim.startswith(utils.cword(v), prefix)
end)
Filters.strict_prefix = mk_filter(function(_, v, prefix)
- local w = utils.cword(v)
- return vim.startswith(w, prefix) and #w ~= #prefix
+ local w = utils.cword(v)
+ return vim.startswith(w, prefix) and #w ~= #prefix
end)
Filters.substr = mk_filter(function(_, v, prefix)
- local w = utils.cword(v)
- local start = w:find(prefix, 1, true)
- return start ~= nil
+ local w = utils.cword(v)
+ local start = w:find(prefix, 1, true)
+ return start ~= nil
end)
return Filters
M lua/complementree/init.lua => lua/complementree/init.lua +127 -121
@@ 5,178 5,184 @@ local sources = require('complementree.sources')
local tsutils = require('nvim-treesitter.ts_utils')
local M = {}
-
-
-
-
-
+---@class CompleteItem
+---@field word string The text that will be inserted
+---@field abbr string? Abbreviation of word
+---@field menu string? Extra text for the popup menu
+---@field info string? More information, can be displayed in a preview window
+---@field kiend string? Single letter indicating the type of completion
+---@field icase boolean Ignore case when comparing for equality of items
+---@field equal boolean Disable filtering of this item
+---@field dup boolean Add this item even if item already present
+---@field empty boolean Add even if it is an empty string
+---@field user_data any? Custom data
local user_config = {
- default = defaults.dummy,
- vim = defaults.ins_completion('C-V'),
+ default = defaults.dummy,
+ vim = defaults.ins_completion('C-V'),
}
function M.setup(config)
- if not config.default then
- error('This config does not have a default key.')
- end
+ if not config.default then
+ error('This config does not have a default key.')
+ end
- local def = config.default
- if not (type(def) == "function") then
- error('Invalid default completion')
- end
+ local def = config.default
+ if not (type(def) == "function") then
+ error('Invalid default completion')
+ end
- user_config = config
+ user_config = config
end
function M.print_config()
- print(vim.inspect(user_config))
+ print(vim.inspect(user_config))
end
local function correct_position(line_to_cursor, linenr)
- local col = vim.fn.match(line_to_cursor, '\\s*\\k*$')
- return linenr - 1, col - 1
+ local col = vim.fn.match(line_to_cursor, '\\s*\\k*$')
+ return linenr - 1, col - 1
end
local function node_type_at_position(l, c)
- local root = tsutils.get_root_for_position(l, c)
- if not root then
- return
- end
+ local root = tsutils.get_root_for_position(l, c)
+ if not root then
+ return
+ end
- local node = root:named_descendant_for_range(l, c, l, c)
- if not node then
- return
- end
+ local node = root:named_descendant_for_range(l, c, l, c)
+ if not node then
+ return
+ end
- return node:type()
+ return node:type()
end
local function get_completion(ft, line_to_cursor, lnum, _col)
- local l, c = correct_position(line_to_cursor, lnum)
- local ft_completion = user_config[ft] or user_config.default
- if ft_completion then
- if type(ft_completion) == "table" then
- local root = tsutils.get_root_for_position(l, c)
-
- for q, sub in pairs(ft_completion) do
-
- if vim.startswith(q, '(') then
- local query = vim.treesitter.query.parse(ft, q)
- for id, node in query:iter_captures(root, 0, l, l + 1) do
- local cname = query.captures[id]
- if tsutils.is_in_node_range(node, l, c) then
- if type(sub) == "table" and sub[cname] then
- return sub[cname]
- elseif type(sub) == "function" then
- return sub
- else
-
- break
- end
- end
- end
- end
- end
-
- local t = node_type_at_position(l, c)
- if not t then
- local def = ft_completion.default
- if type(def) == "function" then
- return def
- else
- error('Invalid default completion source.')
+ local l, c = correct_position(line_to_cursor, lnum)
+ local ft_completion = user_config[ft] or user_config.default
+ if ft_completion then
+ if type(ft_completion) == "table" then
+ local root = tsutils.get_root_for_position(l, c)
+
+ for q, sub in pairs(ft_completion) do
+
+ if vim.startswith(q, '(') then
+ local query = vim.treesitter.query.parse(ft, q)
+ for id, node in query:iter_captures(root, 0, l, l + 1) do
+ local cname = query.captures[id]
+ if tsutils.is_in_node_range(node, l, c) then
+ if type(sub) == "table" and sub[cname] then
+ return sub[cname]
+ elseif type(sub) == "function" then
+ return sub
+ else
+
+ break
+ end
end
- end
- local sub_completion = ft_completion[t] or ft_completion.default
- if sub_completion and type(sub_completion) == "function" then
- return sub_completion
- end
- elseif type(ft_completion) == 'function' then
- return ft_completion
+ end
+ end
+ end
+
+ local t = node_type_at_position(l, c)
+ if not t then
+ local def = ft_completion.default
+ if type(def) == "function" then
+ return def
+ else
+ error('Invalid default completion source.')
+ end
end
- end
+ local sub_completion = ft_completion[t] or ft_completion.default
+ if sub_completion and type(sub_completion) == "function" then
+ return sub_completion
+ end
+ elseif type(ft_completion) == 'function' then
+ return ft_completion
+ end
+ end
end
function M.separate_prefix(line, cursor)
- local line_to_cursor = line:sub(1, cursor)
- local pref_start = line_to_cursor:find('%S*$')
- local prefix = line_to_cursor:sub(pref_start)
+ local line_to_cursor = line:sub(1, cursor)
+ local pref_start = line_to_cursor:find('%S*$')
+ local prefix = line_to_cursor:sub(pref_start)
- return line_to_cursor, pref_start, prefix
+ return line_to_cursor, pref_start, prefix
end
function M.complete()
- if vim.fn.pumvisible() == 0 then
- sources.invalidate_cache()
- end
- if not vim.fn.mode():find('i') then
- return false
- end
+ if vim.fn.pumvisible() == 0 then
+ sources.invalidate_cache()
+ end
+ if not vim.fn.mode():find('i') then
+ return false
+ end
- local bufnr = api.nvim_get_current_buf()
- local ft = api.nvim_buf_get_option(bufnr, 'filetype')
+ local bufnr = api.nvim_get_current_buf()
+ local ft = api.nvim_buf_get_option(bufnr, 'filetype')
- local line = api.nvim_get_current_line()
- local cursor = api.nvim_win_get_cursor(0)
- local lnum = cursor[1]
- local cursor_pos = cursor[2]
- local line_to_cursor, pref_start, _prefix = M.separate_prefix(line, cursor_pos)
+ local line = api.nvim_get_current_line()
+ local cursor = api.nvim_win_get_cursor(0)
+ local lnum = cursor[1]
+ local cursor_pos = cursor[2]
+ local line_to_cursor, pref_start, _prefix = M.separate_prefix(line, cursor_pos)
- local func = get_completion(ft, line_to_cursor, lnum, pref_start)
- if not (func == nil) then
- if func(line_to_cursor, lnum) then
- return true
- end
- end
- return false
+ local func = get_completion(ft, line_to_cursor, lnum, pref_start)
+ if not (func == nil) then
+ if func(line_to_cursor, lnum) then
+ return true
+ end
+ end
+ return false
end
function M._CompleteDone()
- local completed_item = api.nvim_get_vvar('completed_item')
- if not completed_item or not completed_item.user_data or not completed_item.user_data.source then
- return
- end
- local func = sources.complete_done_cbs[completed_item.user_data.source]
- if func then
+ local completed_item = api.nvim_get_vvar('completed_item')
+ if not completed_item or not completed_item.user_data or not completed_item.user_data.source then
+ return
+ end
+ local func = sources.complete_done_cbs[completed_item.user_data.source]
+ if func then
- local previous_opt = api.nvim_get_option('eventignore')
- local newval = previous_opt
- if #newval == 0 then
- newval = 'InsertLeave'
- else
- newval = 'InsertLeave,' .. newval
- end
- api.nvim_set_option('eventignore', newval)
- func(completed_item)
- vim.schedule(function()
- api.nvim_set_option('eventignore', previous_opt)
- end)
- end
+ local previous_opt = api.nvim_get_option('eventignore')
+ local newval = previous_opt
+ if #newval == 0 then
+ newval = 'InsertLeave'
+ else
+ newval = 'InsertLeave,' .. newval
+ end
+ api.nvim_set_option('eventignore', newval)
+ func(completed_item)
+ vim.schedule(function()
+ api.nvim_set_option('eventignore', previous_opt)
+ end)
+ end
end
function M._InsertCharPre()
- if vim.fn.pumvisible() == 1 then
- local char = api.nvim_get_vvar('char')
- if char:find('%s') then
+ if vim.fn.pumvisible() == 1 then
+ local char = api.nvim_get_vvar('char')
+ if char:find('%s') then
- utils.feed('<C-Y>')
- else
+ utils.feed('<C-Y>')
+ else
- vim.schedule(function()
- M.complete()
- end)
- end
- end
+ vim.schedule(function()
+ M.complete()
+ end)
+ end
+ end
end
return M
M lua/complementree/options.lua => lua/complementree/options.lua +2 -2
@@ 1,8 1,8 @@
local M = {}
function M.get(defaults, user)
- vim.tbl_deep_extend('force', defaults, user)
- return defaults
+ vim.tbl_deep_extend('force', defaults, user)
+ return defaults
end
return M
M lua/complementree/sources.lua => lua/complementree/sources.lua +366 -416
@@ 1,40 1,5 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
local Sources = {}
-
-
-
-
-
-
-
-
-
local utils = require('complementree.utils')
local options = require('complementree.options')
local api = vim.api
@@ 43,453 8,438 @@ local lsp = vim.lsp
local cache = {}
function Sources.invalidate_cache()
- cache = {}
+ cache = {}
end
+Sources.complete_done_cbs = {
+}
+
local function cached(kind, func)
- return function(ltc, lnum)
- local m
- local p
- if not cache[kind] then
- m, p = func(ltc, lnum)
- cache[kind] = { m, p }
- else
- m = cache[kind][1]
- p = cache[kind][2]
+ return function(ltc, lnum)
+ local m
+ local p
+ if not cache[kind] then
+ m, p = func(ltc, lnum)
+ cache[kind] = { m, p }
+ else
+ m = cache[kind][1]
+ p = cache[kind][2]
+
+ local pref_start = vim.fn.match(ltc, p .. '\\k*$') + 1
+ if pref_start >= 1 then
+ p = ltc:sub(pref_start)
+ end
+ end
+ local new = {}
+ for _, v in ipairs(m) do
+ table.insert(new, v)
+ end
+ return new, p
+ end
+end
+local apply_snippet = function(...)
+end
+function Sources.lsp_matches(opts)
+ opts = options.get({}, opts)
+ return cached('lsp', function(line_to_cursor, lnum)
+ local function adjust_start_col(line_number, line, items, encoding)
+ local min_start_char = nil
- local pref_start = vim.fn.match(ltc, p .. '\\k*$') + 1
- if pref_start >= 1 then
- p = ltc:sub(pref_start)
- end
+ for _, item in ipairs(items) do
+ if item.textEdit and item.textEdit.range.start.line == line_number - 1 then
+ if min_start_char and min_start_char ~= item.textEdit.range.start.character then
+ return nil
+ end
+ min_start_char = item.textEdit.range.start.character
+ end
+ end
+ if min_start_char then
+ if encoding == 'utf-8' then
+ return min_start_char + 1
+ else
+ return vim.str_byteindex(line, min_start_char, encoding == 'utf-16') + 1
+ end
+ else
+ return nil
end
- local new = {}
- for _, v in ipairs(m) do
- table.insert(new, v)
+ end
+
+ local params = lsp.util.make_position_params()
+ local result_all, err = lsp.buf_request_sync(0, 'textDocument/completion', params)
+ if err then
+ api.nvim_err_writeln(string.format('Error while completing lsp: %s', err))
+ return {}, ''
+ end
+ if not result_all then
+ return {}, ''
+ end
+
+ local matches = {}
+ local start_col = vim.fn.match(line_to_cursor, '\\k*$') + 1
+ for client_id, result in pairs(result_all) do
+ local client = lsp.get_client_by_id(client_id)
+ local items = lsp.util.extract_completion_items(result.result) or {}
+
+ local tmp_col = adjust_start_col(lnum, line_to_cursor, items, client.offset_encoding or 'utf-16')
+ if tmp_col and tmp_col < start_col then
+ start_col = tmp_col
end
- return new, p
- end
-end
+ for _, item in ipairs(items) do
+ local kind = lsp.protocol.CompletionItemKind[item.kind] or ''
+ local word = nil
+ if kind == 'Snippet' then
+ word = item.label
+ elseif item.insertTextFormat == 2 then
+ if item.textEdit then
+ word = item.insertText or item.textEdit.newText
+ elseif item.insertText then
+ if #item.label < #item.insertText then
+ word = item.label
+ else
+ word = item.insertText
+ end
+ else
+ word = item.label
+ end
+ else
+ word = (item.textEdit and item.textEdit.newText) or item.insertText or item.label
+ end
+ local ud = {
+ source = 'lsp',
+ extra = { client_id = client_id, item = item },
+ }
+ table.insert(matches, {
+ word = word,
+ abbr = item.label,
+ kind = kind,
+ menu = item.detail or '',
+ icase = 1,
+ dup = 1,
+ empty = 1,
+ equal = 1,
+ user_data = ud,
+ })
+ end
+ end
+ local prefix = line_to_cursor:sub(start_col)
+ return matches, prefix
+ end)
+end
+local function lsp_completedone(completed_item)
+ -- TODO: over complicated
+ local cursor = api.nvim_win_get_cursor(0)
+ local col = cursor[2]
+ local lnum = cursor[1] - 1
+ local bufnr = api.nvim_get_current_buf()
+
+ local extra = completed_item.user_data.extra
+ local item = extra.item
+
+ local client = lsp.get_client_by_id(extra.client_id)
+ if not client then
+ error(string.format("Could not find client %d", extra.client_id))
+ end
+
+ local expand_snippet = item.insertTextFormat == 2
+ local resolveEdits = (client.server_capabilities.completionProvider or {}).resolveProvider
+ local offset_encoding = client and client.offset_encoding or 'utf-16'
+
+ local tidy = function() end
+ local suffix = nil
+
+ if expand_snippet then
+ local line = api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
+ tidy = function()
+ local start_char = col - #completed_item.word
+ local l = line
+ api.nvim_buf_set_text(bufnr, lnum, start_char, lnum, #l, { '' })
+ end
+ suffix = line:sub(col + 1)
+ end
+
+ if item.additionalTextEdits then
+ tidy()
+ tidy = function() end
+ lsp.util.apply_text_edits(item.additionalTextEdits, bufnr, offset_encoding)
+ elseif resolveEdits and type(item) == 'table' then
+ local v = client.request_sync('completionItem/resolve', item, 1000, bufnr)
+ assert(not v.err, vim.inspect(v.err))
+ local res = v.result
+ if res.additionalTextEdits then
+ tidy()
+ tidy = function() end
+ lsp.util.apply_text_edits(res.additionalTextEdits, bufnr, offset_encoding)
+ end
+ end
+ if expand_snippet then
+ tidy()
+ apply_snippet(item, suffix, lnum)
+ end
+end
+Sources.complete_done_cbs.lsp = lsp_completedone
+-- Conditional loading of the snippets handling / sources
+local lsnip_present, luasnip = pcall(require, "luasnip")
+local snippy_present, snippy = pcall(require, "snippy")
-function Sources.luasnip_matches(opts)
- opts = options.get(opts, {
+if lsnip_present then
+ function Sources.luasnip_matches(opts)
+ opts = options.get(opts, {
exclude_defaults = false,
filetype = nil,
- })
+ })
- local lsnip_present, luasnip = pcall(require, "luasnip")
- if not lsnip_present then
- error("LuaSnip is not installed")
- end
- local function add_snippet(items, s)
+ local function add_snippet(items, s)
table.insert(items, {
- word = s.trigger,
- kind = 'S',
- menu = table.concat(s.description or {}),
- icase = 1,
- dup = 1,
- empty = 1,
- equal = 1,
- user_data = { source = 'luasnip' },
+ word = s.trigger,
+ kind = 'S',
+ menu = table.concat(s.description or {}),
+ icase = 1,
+ dup = 1,
+ empty = 1,
+ equal = 1,
+ user_data = { source = 'luasnip' },
})
- end
+ end
- return cached('luasnip', function(line_to_cursor, _)
+ return cached('luasnip', function(line_to_cursor, _)
local prefix = utils.prefix.lua_regex('%w*$', line_to_cursor)
local items = {}
-
-
-
-
-
-
-
-
-
for ftname, snips in pairs(luasnip.available()) do
- if not (ftname == 'all' and opts.exclude_defaults) then
- vim.tbl_map(function(s)
- add_snippet(items, s)
- end, snips)
- end
+ if not (ftname == 'all' and opts.exclude_defaults) then
+ vim.tbl_map(function(s)
+ add_snippet(items, s)
+ end, snips)
+ end
end
return items, prefix
- end)
-end
-
-
-
+ end)
+ end
-
-
-
-function Sources.lsp_matches(opts)
- opts = options.get({}, opts)
- return cached('lsp', function(line_to_cursor, lnum)
-
-
- local function adjust_start_col(line_number, line, items, encoding)
- local min_start_char = nil
-
- for _, item in ipairs(items) do
- if item.textEdit and item.textEdit.range.start.line == line_number - 1 then
- if min_start_char and min_start_char ~= item.textEdit.range.start.character then
- return nil
- end
- min_start_char = item.textEdit.range.start.character
- end
- end
- if min_start_char then
- if encoding == 'utf-8' then
- return min_start_char + 1
- else
- return vim.str_byteindex(line, min_start_char, encoding == 'utf-16') + 1
- end
- else
- return nil
- end
- end
-
- local params = lsp.util.make_position_params()
- local result_all, err = lsp.buf_request_sync(0, 'textDocument/completion', params)
- if err then
- api.nvim_err_writeln(string.format('Error while completing lsp: %s', err))
- return {}, ''
- end
- if not result_all then
- return {}, ''
- end
-
- local matches = {}
- local start_col = vim.fn.match(line_to_cursor, '\\k*$') + 1
- for client_id, result in pairs(result_all) do
- local client = lsp.get_client_by_id(client_id)
- local items = lsp.util.extract_completion_items(result.result) or {}
-
- local tmp_col = adjust_start_col(lnum, line_to_cursor, items, client.offset_encoding or 'utf-16')
- if tmp_col and tmp_col < start_col then
- start_col = tmp_col
- end
-
- for _, item in ipairs(items) do
- local kind = lsp.protocol.CompletionItemKind[item.kind] or ''
- local word = nil
- if kind == 'Snippet' then
- word = item.label
- elseif item.insertTextFormat == 2 then
- if item.textEdit then
- word = item.insertText or item.textEdit.newText
- elseif item.insertText then
- if #item.label < #item.insertText then
- word = item.label
- else
- word = item.insertText
- end
- else
- word = item.label
- end
- else
- word = (item.textEdit and item.textEdit.newText) or item.insertText or item.label
- end
- local ud = {
- source = 'lsp',
- extra = { client_id = client_id, item = item },
- }
- table.insert(matches, {
- word = word,
- abbr = item.label,
- kind = kind,
- menu = item.detail or '',
- icase = 1,
- dup = 1,
- empty = 1,
- equal = 1,
- user_data = ud,
- })
- end
- end
- local prefix = line_to_cursor:sub(start_col)
- return matches, prefix
- end)
-end
-
-local function apply_snippet(item, suffix, lnum)
- local luasnip = require('luasnip')
- if item.textEdit then
+ apply_snippet = function(item, suffix, lnum)
+ local luasnip = require('luasnip')
+ if item.textEdit then
luasnip.lsp_expand(item.textEdit.newText .. suffix)
- elseif item.insertText then
+ elseif item.insertText then
luasnip.lsp_expand(item.insertText .. suffix)
- elseif item.label then
+ elseif item.label then
luasnip.lsp_expand(item.label .. suffix)
- end
- vim.schedule(function()
+ end
+ vim.schedule(function()
local curline = api.nvim_get_current_line()
if vim.endswith(curline, suffix) and not luasnip.get_active_snip() then
- local newcol = #curline - #suffix
- api.nvim_win_set_cursor(0, { lnum + 1, newcol })
+ local newcol = #curline - #suffix
+ api.nvim_win_set_cursor(0, { lnum + 1, newcol })
end
- end)
+ end)
+ end
+
+ Sources.complete_done_cbs.luasnip = function(_)
+ if require('luasnip').expandable() then
+ require('luasnip').expand()
+ end
+ end
+
+elseif snippy_present then
+ Sources.complete_done_cbs.lsp = function(completed_item)
+ local lsp_item = completed_item.user_data.extra.item
+ local snippet
+ if lsp_item.textEdit then
+ snippet = lsp_item.textEdit.newText
+ elseif lsp_item.insertTextFormat == 2 then
+ snippet = lsp_item.insertText
+ end
+ if snippet then
+ snippy.expand_snippet(snippet, completed_item.word)
+ end
+ end
end
local ctags_extension = {
- default = {
- ['c'] = 'class',
- ['d'] = 'define',
- ['e'] = 'enumerator',
- ['f'] = 'function',
- ['F'] = 'file',
- ['g'] = 'enumeration',
- ['m'] = 'member',
- ['p'] = 'prototype',
- ['s'] = 'structure',
- ['t'] = 'typedef',
- ['u'] = 'union',
- ['v'] = 'variable',
- },
+ default = {
+ ['c'] = 'class',
+ ['d'] = 'define',
+ ['e'] = 'enumerator',
+ ['f'] = 'function',
+ ['F'] = 'file',
+ ['g'] = 'enumeration',
+ ['m'] = 'member',
+ ['p'] = 'prototype',
+ ['s'] = 'structure',
+ ['t'] = 'typedef',
+ ['u'] = 'union',
+ ['v'] = 'variable',
+ },
}
function Sources.ctags_matches(opts)
- opts = options.get({}, opts)
- return cached('ctags', function(line_to_cursor, _)
- local prefix = utils.prefix.vim_keyword(line_to_cursor)
-
- local filetype = api.nvim_buf_get_option(0, 'filetype')
- local extensions = ctags_extension[filetype] or ctags_extension.default
- local tags = vim.fn.taglist('.*')
-
- local items = {}
- for _, t in ipairs(tags) do
- local ud = { source = 'ctags' }
- items[#items + 1] = {
- word = t.name,
- kind = (t.kind and extensions[t.kind] or 'undefined'),
- icase = 1,
- dup = 0,
- equal = 1,
- empty = 1,
- user_data = ud,
- }
- end
-
- return items, prefix
- end)
+ opts = options.get({}, opts)
+ return cached('ctags', function(line_to_cursor, _)
+ local prefix = utils.prefix.vim_keyword(line_to_cursor)
+
+ local filetype = api.nvim_buf_get_option(0, 'filetype')
+ local extensions = ctags_extension[filetype] or ctags_extension.default
+ local tags = vim.fn.taglist('.*')
+
+ local items = {}
+ for _, t in ipairs(tags) do
+ local ud = { source = 'ctags' }
+ items[#items + 1] = {
+ word = t.name,
+ kind = (t.kind and extensions[t.kind] or 'undefined'),
+ icase = 1,
+ dup = 0,
+ equal = 1,
+ empty = 1,
+ user_data = ud,
+ }
+ end
+
+ return items, prefix
+ end)
end
-
-
local os_name = string.lower(jit.os)
local is_linux = (os_name == 'linux' or os_name == 'osx' or os_name == 'bsd')
local os_sep = is_linux and '/' or '\\'
local os_path = '[' .. os_sep .. '%w+%-%.%_]*$'
function Sources.filepath_matches(opts)
- local relpath = utils.make_relative_path
- local config = options.get({
- show_hidden = false,
- ignore_directories = true,
- max_depth = math.huge,
- relative_paths = false,
- ignore_pattern = '',
- root_dirs = { '.' },
- }, opts)
-
- local function iter_files()
- local path_stack = vim.fn.reverse(config.root_dirs or { '.' })
- local iter_stack = {}
- for _, p in ipairs(path_stack) do
- table.insert(iter_stack, vim.loop.fs_scandir(p))
+ local relpath = utils.make_relative_path
+ local config = options.get({
+ show_hidden = false,
+ ignore_directories = true,
+ max_depth = math.huge,
+ relative_paths = false,
+ ignore_pattern = '',
+ root_dirs = { '.' },
+ }, opts)
+
+ local function iter_files()
+ local path_stack = vim.fn.reverse(config.root_dirs or { '.' })
+ local iter_stack = {}
+ for _, p in ipairs(path_stack) do
+ table.insert(iter_stack, vim.loop.fs_scandir(p))
+ end
+
+ if config.max_depth == 0 then
+ return function()
+ return nil
end
-
- if config.max_depth == 0 then
- return function()
+ end
+
+ return function()
+ local iter = iter_stack[#iter_stack]
+ local path = path_stack[#path_stack]
+ while true do
+ local next_path, path_type = vim.loop.fs_scandir_next(iter)
+
+ if not next_path then
+ table.remove(iter_stack)
+ table.remove(path_stack)
+ if #iter_stack == 0 then
return nil
- end
- end
-
- return function()
- local iter = iter_stack[#iter_stack]
- local path = path_stack[#path_stack]
- while true do
- local next_path, path_type = vim.loop.fs_scandir_next(iter)
-
- if not next_path then
- table.remove(iter_stack)
- table.remove(path_stack)
- if #iter_stack == 0 then
- return nil
- end
- iter = iter_stack[#iter_stack]
- path = path_stack[#path_stack]
- elseif
-(vim.startswith(next_path, '.') and not config.show_hidden) or
- (#config.ignore_pattern > 0 and string.find(next_path, config.ignore_pattern) ~= nil) then
-
- next_path = nil
- path_type = nil
- else
- local full_path = path .. os_sep .. next_path
- if path_type == 'directory' then
- if #iter_stack < config.max_depth then
- iter = vim.loop.fs_scandir(full_path)
- path = full_path
- table.insert(path_stack, full_path)
- table.insert(iter_stack, iter)
- end
- if not config.ignore_directories then
- return full_path
- end
- else
- return full_path
- end
+ end
+ iter = iter_stack[#iter_stack]
+ path = path_stack[#path_stack]
+ elseif
+ (vim.startswith(next_path, '.') and not config.show_hidden) or
+ (#config.ignore_pattern > 0 and string.find(next_path, config.ignore_pattern) ~= nil) then
+
+ next_path = nil
+ path_type = nil
+ else
+ local full_path = path .. os_sep .. next_path
+ if path_type == 'directory' then
+ if #iter_stack < config.max_depth then
+ iter = vim.loop.fs_scandir(full_path)
+ path = full_path
+ table.insert(path_stack, full_path)
+ table.insert(iter_stack, iter)
end
- end
- end
- end
-
- return cached('filepath', function(line_to_cursor, _)
- local prefix = utils.prefix.lua_regex(os_path, line_to_cursor)
-
- local cwd = vim.fn.getcwd()
- local fpath
- local matches = {}
- for path in iter_files() do
- fpath = config.relative_paths and relpath(path, cwd) or path
-
- matches[#matches + 1] = {
- word = fpath,
- abbr = fpath,
- kind = '[path]',
- icase = 1,
- dup = 1,
- empty = 1,
- equal = 1,
- user_data = { source = 'filepath' },
- }
+ if not config.ignore_directories then
+ return full_path
+ end
+ else
+ return full_path
+ end
+ end
end
-
- return matches, prefix
- end)
+ end
+ end
+
+ return cached('filepath', function(line_to_cursor, _)
+ local prefix = utils.prefix.lua_regex(os_path, line_to_cursor)
+
+ local cwd = vim.fn.getcwd()
+ local fpath
+ local matches = {}
+ for path in iter_files() do
+ fpath = config.relative_paths and relpath(path, cwd) or path
+
+ matches[#matches + 1] = {
+ word = fpath,
+ abbr = fpath,
+ kind = '[path]',
+ icase = 1,
+ dup = 1,
+ empty = 1,
+ equal = 1,
+ user_data = { source = 'filepath' },
+ }
+ end
+
+ return matches, prefix
+ end)
end
-
-
local tslocals = require('nvim-treesitter.locals')
function Sources.treesitter_matches(opts)
- local _config = options.get({}, opts)
+ local _config = options.get({}, opts)
- return cached('treesitter', function(line_to_cursor, _lnum)
- local prefix = utils.prefix.lua_regex('%S*$', line_to_cursor)
- local defs = tslocals.get_definitions(0)
+ return cached('treesitter', function(line_to_cursor, _lnum)
+ local prefix = utils.prefix.lua_regex('%S*$', line_to_cursor)
+ local defs = tslocals.get_definitions(0)
- local items = {}
+ local items = {}
- for _, def in ipairs(defs) do
+ for _, def in ipairs(defs) do
- local node
- local kind
- for k, cap in pairs(def) do
- if k ~= 'associated' then
- node = cap.node
- kind = k
- break
- end
- end
-
- if node then
- items[#items + 1] = {
- word = vim.treesitter.get_node_text(node, 0),
- kind = kind,
- icase = 1,
- dup = 0,
- empty = 1,
- equal = 1,
- user_data = { source = 'treesitter' },
- }
- end
+ local node
+ local kind
+ for k, cap in pairs(def) do
+ if k ~= 'associated' then
+ node = cap.node
+ kind = k
+ break
+ end
end
- return items, prefix
- end)
-end
-
-
-
-local function lsp_completedone(completed_item)
- local cursor = api.nvim_win_get_cursor(0)
- local col = cursor[2]
- local lnum = cursor[1] - 1
- local bufnr = api.nvim_get_current_buf()
-
- local extra = completed_item.user_data.extra
- local item = extra.item
-
- local client = lsp.get_client_by_id(extra.client_id)
- if not client then
- error(string.format("Could not find client %d", extra.client_id))
- end
-
- local expand_snippet = item.insertTextFormat == 2
- local resolveEdits = (client.server_capabilities.completionProvider or {}).resolveProvider
- local offset_encoding = client and client.offset_encoding or 'utf-16'
-
- local tidy = function() end
- local suffix = nil
-
- if expand_snippet then
- local line = api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
- tidy = function()
-
- local start_char = col - #completed_item.word
- local l = line
- api.nvim_buf_set_text(bufnr, lnum, start_char, lnum, #l, { '' })
- end
- suffix = line:sub(col + 1)
- end
-
- if item.additionalTextEdits then
- tidy()
- lsp.util.apply_text_edits(item.additionalTextEdits, bufnr, offset_encoding)
- if expand_snippet then
- apply_snippet(item, suffix, lnum)
+ if node then
+ items[#items + 1] = {
+ word = vim.treesitter.get_node_text(node, 0),
+ kind = kind,
+ icase = 1,
+ dup = 0,
+ empty = 1,
+ equal = 1,
+ user_data = { source = 'treesitter' },
+ }
end
- elseif resolveEdits and type(item) == 'table' then
- local v = client.request_sync('completionItem/resolve', item, 1000, bufnr)
- assert(not v.err, vim.inspect(v.err))
- local res = v.result
- if res.additionalTextEdits then
- tidy()
- tidy = function() end
- lsp.util.apply_text_edits(res.additionalTextEdits, bufnr, offset_encoding)
- end
- if expand_snippet then
- tidy()
- apply_snippet(item, suffix, lnum)
- end
- elseif expand_snippet then
- tidy()
- apply_snippet(item, suffix, lnum)
- end
-end
+ end
-local function luasnip_completedone(_)
- if require('luasnip').expandable() then
- require('luasnip').expand()
- end
+ return items, prefix
+ end)
end
-Sources.complete_done_cbs = {
- lsp = lsp_completedone,
- luasnip = luasnip_completedone,
-}
-
return Sources
M lua/complementree/utils.lua => lua/complementree/utils.lua +24 -18
@@ 2,41 2,47 @@ local api = vim.api
local Prefix = {}
-
function Prefix.lua_regex(regex, line)
- local pref_start = line:find(regex)
- local prefix = line:sub(pref_start)
+ local pref_start = line:find(regex)
+ local prefix = line:sub(pref_start)
- return prefix
+ return prefix
end
function Prefix.vim_keyword(line)
- local pref_start = vim.fn.match(line, '\\k*$') + 1
- local prefix = line:sub(pref_start)
+ local pref_start = vim.fn.match(line, '\\k*$') + 1
+ local prefix = line:sub(pref_start)
- return prefix
+ return prefix
end
local Utils = {}
-
-
+--- Feed a bunch of keys to nvim
+---@param codes string
function Utils.feed(codes)
- api.nvim_feedkeys(api.nvim_replace_termcodes(codes, true, true, true), 'm', true)
+ api.nvim_feedkeys(api.nvim_replace_termcodes(codes, true, true, true), 'm', true)
end
+--- Gets the word to use for completion
+---@param complete_item CompleteItem
+---@return string
function Utils.cword(complete_item)
- return (complete_item.abbr or complete_item.word)
+ return (complete_item.abbr or complete_item.word)
end
+--- Makes the provided path relative to root
+---@param path string Path to make relative
+---@param root sring Root path
+---@return string
function Utils.make_relative_path(path, root)
- if vim.startswith(path, root) then
- local baselen = #root
- if path:sub(0, baselen) == root then
- path = path:sub(baselen + 2)
- end
- end
- return path
+ if vim.startswith(path, root) then
+ local baselen = #root
+ if path:sub(0, baselen) == root then
+ path = path:sub(baselen + 2)
+ end
+ end
+ return path
end
Utils.prefix = Prefix
D teal/complementree/combinators.tl => teal/complementree/combinators.tl +0 -87
@@ 1,87 0,0 @@
-local M = {}
-
-local function complete(col: integer, matches: {CompleteItem}): boolean
- if matches and #matches > 0 then
- vim.fn.complete(col, matches)
- return true
- else
- return false
- end
-end
-
-function M.combine(...: Source): Source
- local funcs = { ... }
- return function(ltc: string, lnum: integer): {CompleteItem}, string
- local matches: {CompleteItem} = {}
- local coherent_p: string
- for _, f in ipairs(funcs) do
- local m, p = f(ltc, lnum)
- if not coherent_p then
- coherent_p = p
- end
-
- if coherent_p == p then
- vim.list_extend(matches, m)
- end
- end
- return matches, coherent_p
- end
-end
-
-function M.optional(mandat: Source, opt: Source): Source
- return function(ltc: string, lnum: integer): {CompleteItem}, string
- local matches, prefix = mandat(ltc, lnum)
- if #matches > 0 then
- local m, p = opt(ltc, lnum)
- if p == prefix then
- vim.list_extend(matches, m)
- end
- return matches, prefix
- else
- return {}, ''
- end
- end
-end
-
--- TODO(vigoux): do we even need this anymore ?
-function M.non_empty_prefix(func: Source): Completor
- return function(ltc: string, lnum: integer): boolean
- local compl, prefix = func(ltc, lnum)
- if #prefix > 1 then
- return complete(#ltc - #prefix + 1, compl)
- else
- return false
- end
- end
-end
-
-function M.wrap(func: Source): Completor
- return function(ltc: string, lnum: integer): boolean
- local compl, prefix = func(ltc, lnum)
- return complete(#ltc - #prefix + 1, compl)
- end
-end
-
-function M.pipeline(source: Source, ...: Pipe): Completor
- local current = source
- for _, func in ipairs { ... } do
- current = func(current)
- end
-
- return M.wrap(current)
-end
-
-function M.chain(...: Source): Source
- local funcs = { ... }
- return function(ltc: string, lnum: integer): {CompleteItem}, string
- for _, f in ipairs(funcs) do
- local c, pref = f(ltc, lnum)
- if #c > 0 then
- return c, pref
- end
- end
- return {}, ''
- end
-end
-
-return M
D teal/complementree/comparators.tl => teal/complementree/comparators.tl +0 -65
@@ 1,65 0,0 @@
-local utils = require 'complementree.utils'
-
-local record Comparators
- alphabetic: Pipe
- length: Pipe
- fzy: Pipe
- ifzy: Pipe
-end
-
-local type Comparator = function(string, string): boolean
-
-local function mk_comparator(func: Comparator): Pipe
- return function(msource: Source): Source
- return function(ltc: string, lnum: integer): {CompleteItem}, string
- local orig, prefix = msource(ltc, lnum)
- local cmp_cache = {}
- table.sort(orig, function(a: CompleteItem, b: CompleteItem): boolean
- local key = { a, b }
-
- if not cmp_cache[key] then
- cmp_cache[key] = func(utils.cword(a), utils.cword(b))
- end
-
- return cmp_cache[key]
- end)
- return orig, prefix
- end
- end
-end
-
-Comparators.alphabetic = mk_comparator(function(a: string, b: string): boolean
- return a < b
-end)
-
-Comparators.length = mk_comparator(function(a: string, b: string): boolean
- return #a < #b
-end)
-
-local ok_fzy, fzy = pcall(require, 'fzy')
-if ok_fzy then
- function Comparators.fzy(msource: Source): Source
- return function (ltc: string, lnum: integer): {CompleteItem}, string
- local orig, prefix = msource(ltc, lnum)
- local scores: {CompleteItem:number} = {}
- local matching: {CompleteItem} = {}
- if prefix ~= "" then
- for _, a in ipairs(orig) do
- local s, _ = fzy.match(prefix, utils.cword(a))
- if math.abs(s or math.huge) ~= math.huge or prefix == utils.cword(a) then
- scores[a] = s
- table.insert(matching, a)
- end
- end
- table.sort(matching, function(a: CompleteItem, b: CompleteItem): boolean
- return scores[a] > scores[b]
- end)
- return matching, prefix
- else
- return orig, prefix
- end
- end
- end
-end
-
-return Comparators
D teal/complementree/defaults.tl => teal/complementree/defaults.tl +0 -38
@@ 1,38 0,0 @@
-local record Defaults
- ins_completion: function(string): Completor
- dummy: Completor
- luasnip: Completor
- lsp: Completor
- ctags: Completor
- filepath: Completor
- treesitter: Completor
-end
-
-local comb = require 'complementree.combinators'
-local sources = require 'complementree.sources'
-local filters = require 'complementree.filters'
-local comp = require 'complementree.comparators'
-local utils = require 'complementree.utils'
-
-function Defaults.ins_completion(mode: string): Completor
- return function(): boolean
- utils.feed(string.format('<C-X><%s>', mode))
- return vim.fn.pumvisible() == 1
- end
-end
-
-function Defaults.dummy(_: string, _: integer): boolean
- -- Does nothing
-end
-
-Defaults.luasnip = comb.pipeline(sources.luasnip_matches {}, filters.prefix, comp.alphabetic)
-
-Defaults.lsp = comb.pipeline(sources.lsp_matches {}, filters.prefix, comp.alphabetic)
-
-Defaults.ctags = comb.pipeline(sources.ctags_matches {}, filters.prefix, comp.alphabetic)
-
-Defaults.filepath = comb.pipeline(sources.filepath_matches {}, filters.substr, comp.alphabetic)
-
-Defaults.treesitter = comb.pipeline(sources.treesitter_matches {}, filters.substr, comp.alphabetic)
-
-return Defaults
D teal/complementree/filters.tl => teal/complementree/filters.tl +0 -49
@@ 1,49 0,0 @@
-local utils = require 'complementree.utils'
-
-local record Filters
- amount: function(integer): Pipe
- prefix: Pipe
- strict_prefix: Pipe
- substr: Pipe
-end
-
-local type Filter = function(integer, CompleteItem, string): boolean
-
--- A function that returns a function that returns a function
-local function mk_filter(func: Filter): Pipe
- return function(msource: Source): Source
- return function(ltc: string, lnum: integer): {CompleteItem}, string
- local orig, prefix = msource(ltc, lnum)
- local filtered: {CompleteItem} = {}
- for i, v in ipairs(orig) do
- if func(i, v, prefix) then
- table.insert(filtered, v)
- end
- end
- return filtered, prefix
- end
- end
-end
-
-function Filters.amount(n: integer): Pipe
- return mk_filter(function(i: integer, _: CompleteItem, _: string): boolean
- return i <= n
- end)
-end
-
-Filters.prefix = mk_filter(function(_: integer, v: CompleteItem, prefix: string): boolean
- return vim.startswith(utils.cword(v), prefix)
-end)
-
-Filters.strict_prefix = mk_filter(function(_: integer, v: CompleteItem, prefix: string): boolean
- local w = utils.cword(v)
- return vim.startswith(w, prefix) and #w ~= #prefix
-end)
-
-Filters.substr = mk_filter(function(_: integer, v: CompleteItem, prefix: string): boolean
- local w = utils.cword(v)
- local start = w:find(prefix, 1, true)
- return start ~= nil
-end)
-
-return Filters
D teal/complementree/init.tl => teal/complementree/init.tl +0 -182
@@ 1,182 0,0 @@
-local api = vim.api
-local defaults = require 'complementree.defaults'
-local utils = require 'complementree.utils'
-local sources = require 'complementree.sources'
-local tsutils = require 'nvim-treesitter.ts_utils'
-local M = {}
-
--- Explanation of the Completor nesting:
--- 1. Same completion for all ft
--- 2. Completion based on node type
--- 3. Completion based on query macthing
-local type UserConfig = {string: Completor|{string:Completor|{string:Completor}}}
-
-local user_config: UserConfig = {
- default = defaults.dummy,
- vim = defaults.ins_completion 'C-V',
-}
-
-function M.setup(config: UserConfig)
- if not config.default then
- error 'This config does not have a default key.'
- end
-
- local def = config.default
- if not def is Completor then
- error 'Invalid default completion'
- end
-
- user_config = config
-end
-
-function M.print_config()
- print(vim.inspect(user_config))
-end
-
-local function correct_position(line_to_cursor: string, linenr: integer): integer, integer
- local col = vim.fn.match(line_to_cursor, '\\s*\\k*$')
- return linenr - 1, col - 1
-end
-
-local function node_type_at_position(l: integer, c: integer): string
- local root = tsutils.get_root_for_position(l, c)
- if not root then
- return
- end
-
- local node = root:named_descendant_for_range(l, c, l, c)
- if not node then
- return
- end
-
- return node:type()
-end
-
-local function get_completion(ft: string, line_to_cursor: string, lnum: integer, _col: integer): Completor|nil
- local l, c = correct_position(line_to_cursor, lnum)
- local ft_completion = user_config[ft] or user_config.default
- if ft_completion then
- if ft_completion is {string: Completor|{string:Completor}} then
- local root = tsutils.get_root_for_position(l, c)
- -- Before attempting to match the node type, try query based filters
- for q, sub in pairs(ft_completion) do
- -- Queries always start with a parenthesis
- if vim.startswith(q, '(') then
- local query = vim.treesitter.query.parse(ft, q)
- for id, node in query:iter_captures(root, 0, l, l + 1) do
- local cname = query.captures[id]
- if tsutils.is_in_node_range(node, l, c) then
- if sub is {string:Completor} and sub[cname] then
- return sub[cname]
- elseif sub is Completor then
- return sub
- else
- -- We will definitely not be able to do anything with this source
- break
- end
- end
- end
- end
- end
-
- local t = node_type_at_position(l, c)
- if not t then
- local def = ft_completion.default
- if def is Completor then
- return def
- else
- error 'Invalid default completion source.'
- end
- end
- local sub_completion = ft_completion[t] or ft_completion.default
- if sub_completion and sub_completion is Completor then
- return sub_completion
- end
- elseif type(ft_completion) == 'function' then
- return ft_completion
- end
- end
-end
-
-function M.separate_prefix(line: string, cursor: integer): string, integer, string
- local line_to_cursor = line:sub(1, cursor)
- local pref_start = line_to_cursor:find '%S*$'
- local prefix = line_to_cursor:sub(pref_start)
-
- return line_to_cursor, pref_start, prefix
-end
-
-function M.complete(): boolean
- -- Only refresh when not restarting
- if vim.fn.pumvisible() == 0 then
- sources.invalidate_cache()
- end
- if not vim.fn.mode():find 'i' then
- return false
- end
-
- local bufnr = api.nvim_get_current_buf()
- local ft = api.nvim_buf_get_option(bufnr, 'filetype') as string
-
- local line = api.nvim_get_current_line()
- local cursor = api.nvim_win_get_cursor(0)
- local lnum = cursor[1]
- local cursor_pos = cursor[2]
- local line_to_cursor, pref_start, _prefix = M.separate_prefix(line, cursor_pos)
-
- -- The source signature is
- -- line_content, line_content_up_to_cursor, prefix, column
-
- local func = get_completion(ft, line_to_cursor, lnum, pref_start)
- if not func is nil then
- if func(line_to_cursor, lnum) then
- return true
- end
- end
- return false
-end
-
-function M._CompleteDone()
- local completed_item = api.nvim_get_vvar 'completed_item' as CompleteItem
- if not completed_item or not completed_item.user_data or not completed_item.user_data.source then
- return
- end
- local func = sources.complete_done_cbs[completed_item.user_data.source]
- if func then
- -- We will force the ignore of InsertLeave events for a certain time, in order to avoid strange
- -- behavior and flickering of the UI. So we _synchronously_ set 'eventignore' to ignore
- -- InsertLeave, and schedule the reset in the loop.
- --
- -- The reset is scheduled during this time, so that when the user's events start to be
- -- processed, everything will be just as if nothing happened.
- local previous_opt = api.nvim_get_option 'eventignore' as string
- local newval = previous_opt
- if #newval == 0 then
- newval = 'InsertLeave'
- else
- newval = 'InsertLeave,' .. newval
- end
- api.nvim_set_option('eventignore', newval)
- func(completed_item)
- vim.schedule(function()
- api.nvim_set_option('eventignore', previous_opt)
- end)
- end
-end
-
-function M._InsertCharPre()
- if vim.fn.pumvisible() == 1 then
- local char = api.nvim_get_vvar 'char' as string
- if char:find '%s' then
- -- Whitespace, so accept this choice and stop here
- utils.feed '<C-Y>'
- else
- -- Refresh completion after this char is inserted
- vim.schedule(function()
- M.complete()
- end)
- end
- end
-end
-
-return M
D teal/complementree/options.tl => teal/complementree/options.tl +0 -8
@@ 1,8 0,0 @@
-local M = {}
-
-function M.get<T>(defaults: T, user: T): T
- vim.tbl_deep_extend('force', defaults as {string: any}, user as {string: any})
- return defaults
-end
-
-return M
D teal/complementree/sources.tl => teal/complementree/sources.tl +0 -495
@@ 1,495 0,0 @@
-local record LspOptions
- -- Empty
-end
-
-local record LuasnipOptions
- exclude_defaults: boolean
- filetype: string
-end
-
-local record CtagsOptions
- -- Empty
-end
-
-local record FilepathOptions
- show_hidden: boolean
- ignore_directories: boolean
- max_depth: integer
- relative_paths: boolean
- ignore_pattern: string
- root_dirs: {string}
-end
-
-local record TreesitterOptions
- -- Empty
-end
-
-local record Sources
- lsp_matches: function(LspOptions): Source
- luasnip_matches: function(LuasnipOptions): Source
- ctags_matches: function(CtagsOptions): Source
- filepath_matches: function(FilepathOptions): Source
- treesitter_matches: function(TreesitterOptions): Source
-
- -- Internal
- complete_done_cbs: {string:function(CompleteItem)}
-end
-
-local utils = require 'complementree.utils'
-local options = require 'complementree.options'
-local api = vim.api
-local lsp = vim.lsp
-
-local cache: {string: {{CompleteItem}, string}} = {}
-
-function Sources.invalidate_cache()
- cache = {}
-end
-
-local function cached(kind: string, func: Source): Source
- return function(ltc: string, lnum: integer): {CompleteItem}, string
- local m: {CompleteItem}
- local p: string
- if not cache[kind] then
- m, p = func(ltc, lnum)
- cache[kind] = { m, p }
- else
- m = cache[kind][1]
- p = cache[kind][2]
- -- We need to correct the prefix now
- -- in order to include the added character
- -- FIXME(vigoux): this is not right, we lose the whole "prefix resolution" thing by
- -- only using a regex here. But I think it is fine, for performanace reasons
- local pref_start = vim.fn.match(ltc, p .. '\\k*$') + 1
- if pref_start >= 1 then
- p = ltc:sub(pref_start)
- end
- end
- local new = {}
- for _, v in ipairs(m) do
- table.insert(new, v)
- end
- return new, p
- end
-end
-
--- Options:
---
--- filetype: forces the filetype as a source
--- exclude_default: don't include the default snippets
-function Sources.luasnip_matches(opts: LuasnipOptions): Source
- opts = options.get(opts, {
- exclude_defaults = false,
- filetype = nil,
- })
-
- local lsnip_present,luasnip = pcall(require, "luasnip")
- if not lsnip_present then
- error("LuaSnip is not installed")
- end
-
- local function add_snippet(items: {CompleteItem}, s: luasnip.Snippet)
- table.insert(items, {
- word = s.trigger,
- kind = 'S',
- menu = table.concat(s.description or {}),
- icase = 1,
- dup = 1,
- empty = 1,
- equal = 1,
- user_data = { source = 'luasnip' },
- })
- end
-
- return cached('luasnip', function(line_to_cursor: string, _: integer): {CompleteItem}, string
- local prefix = utils.prefix.lua_regex('%w*$', line_to_cursor)
- local items: {CompleteItem} = {}
-
- -- Luasnip format:
- -- {
- -- description = table(string),
- -- name = string,
- -- regTrig = bool,
- -- trigger = string,
- -- wordTrig = bool
- -- }
-
- for ftname, snips in pairs(luasnip.available()) do
- if not (ftname == 'all' and opts.exclude_defaults) then
- vim.tbl_map(function(s: luasnip.Snippet)
- add_snippet(items, s)
- end, snips)
- end
- end
-
- return items, prefix
- end)
-end
-
-local record LspExtraInfo
- client_id: integer
- item: lsp.LspCompletionItem
-end
-
--- Shamelessly stollen from https://github.com/mfussenegger/nvim-lsp-compl with small adaptations
-function Sources.lsp_matches(opts: LspOptions): Source
- opts = options.get({} as LspOptions, opts)
- return cached('lsp', function(line_to_cursor: string, lnum: integer): {CompleteItem}, string
- -- For lsp determining the preffix is painful, but thanks to the great @mfussenegger, we can fix
- -- this all !
- local function adjust_start_col(line_number: integer, line: string, items: {lsp.LspCompletionItem}, encoding: string): integer|nil
- local min_start_char: integer = nil
-
- for _, item in ipairs(items) do
- if item.textEdit and item.textEdit.range.start.line == line_number - 1 then
- if min_start_char and min_start_char ~= item.textEdit.range.start.character then
- return nil
- end
- min_start_char = item.textEdit.range.start.character
- end
- end
- if min_start_char then
- if encoding == 'utf-8' then
- return min_start_char + 1
- else
- return vim.str_byteindex(line, min_start_char, encoding == 'utf-16') + 1
- end
- else
- return nil
- end
- end
-
- local params = lsp.util.make_position_params()
- local result_all, err = lsp.buf_request_sync(0, 'textDocument/completion', params)
- if err then
- api.nvim_err_writeln(string.format('Error while completing lsp: %s', err))
- return {}, ''
- end
- if not result_all then
- return {}, ''
- end
-
- local matches = {}
- local start_col = vim.fn.match(line_to_cursor, '\\k*$') + 1
- for client_id, result in pairs(result_all) do
- local client = lsp.get_client_by_id(client_id)
- local items = lsp.util.extract_completion_items(result.result) or {}
-
- local tmp_col = adjust_start_col(lnum, line_to_cursor, items, client.offset_encoding or 'utf-16')
- if tmp_col and tmp_col < start_col then
- start_col = tmp_col
- end
-
- for _, item in ipairs(items) do
- local kind = lsp.protocol.CompletionItemKind[item.kind] or ''
- local word: string = nil
- if kind == 'Snippet' then
- word = item.label
- elseif item.insertTextFormat == 2 then
- if item.textEdit then
- word = item.insertText or item.textEdit.newText
- elseif item.insertText then
- if #item.label < #item.insertText then
- word = item.label
- else
- word = item.insertText
- end
- else
- word = item.label
- end
- else
- word = (item.textEdit and item.textEdit.newText) or item.insertText or item.label
- end
- local ud: CompleteExtraInfo = {
- source = 'lsp',
- extra = { client_id = client_id, item = item } as LspExtraInfo
- }
- table.insert(matches, {
- word = word,
- abbr = item.label,
- kind = kind,
- menu = item.detail or '',
- icase = 1,
- dup = 1,
- empty = 1,
- equal = 1,
- user_data = ud,
- })
- end
- end
- local prefix = line_to_cursor:sub(start_col)
- return matches, prefix
- end)
-end
-
-local function apply_snippet(item: lsp.LspCompletionItem, suffix: string, lnum: integer)
- local luasnip = require 'luasnip'
- if item.textEdit then
- luasnip.lsp_expand(item.textEdit.newText .. suffix)
- elseif item.insertText then
- luasnip.lsp_expand(item.insertText .. suffix)
- elseif item.label then
- luasnip.lsp_expand(item.label .. suffix)
- end
- vim.schedule(function()
- local curline = api.nvim_get_current_line()
- if vim.endswith(curline, suffix) and not luasnip.get_active_snip() then
- local newcol = #curline - #suffix
- api.nvim_win_set_cursor(0, { lnum + 1, newcol })
- end
- end)
-end
-
-local ctags_extension: {string: {string:string}} = {
- default = {
- ['c'] = 'class',
- ['d'] = 'define',
- ['e'] = 'enumerator',
- ['f'] = 'function',
- ['F'] = 'file',
- ['g'] = 'enumeration',
- ['m'] = 'member',
- ['p'] = 'prototype',
- ['s'] = 'structure',
- ['t'] = 'typedef',
- ['u'] = 'union',
- ['v'] = 'variable',
- },
-}
-
-function Sources.ctags_matches(opts: CtagsOptions): Source
- opts = options.get({} as CtagsOptions, opts)
- return cached('ctags', function(line_to_cursor: string, _: integer): {CompleteItem}, string
- local prefix = utils.prefix.vim_keyword(line_to_cursor)
-
- local filetype = api.nvim_buf_get_option(0, 'filetype') as string
- local extensions = ctags_extension[filetype] or ctags_extension.default
- local tags = vim.fn.taglist '.*'
-
- local items: {CompleteItem} = {}
- for _, t in ipairs(tags) do
- local ud: CompleteExtraInfo = { source = 'ctags' }
- items[#items + 1] = {
- word = t.name,
- kind = (t.kind and extensions[t.kind] or 'undefined'),
- icase = 1,
- dup = 0,
- equal = 1,
- empty = 1,
- user_data = ud,
- }
- end
-
- return items, prefix
- end)
-end
-
---
-
-local os_name = string.lower(jit.os)
-local is_linux = (os_name == 'linux' or os_name == 'osx' or os_name == 'bsd')
-local os_sep = is_linux and '/' or '\\'
-local os_path = '[' .. os_sep .. '%w+%-%.%_]*$'
-
-function Sources.filepath_matches(opts: FilepathOptions): Source
- local relpath = utils.make_relative_path
- local config = options.get({
- show_hidden = false,
- ignore_directories = true,
- max_depth = math.huge,
- relative_paths = false,
- ignore_pattern = '',
- root_dirs = { '.' },
- }, opts)
-
- local function iter_files(): function(): string|nil
- local path_stack: {string} = vim.fn.reverse(config.root_dirs or { '.' })
- local iter_stack = {}
- for _, p in ipairs(path_stack) do
- table.insert(iter_stack, vim.loop.fs_scandir(p))
- end
-
- if config.max_depth == 0 then
- return function(): string|nil
- return nil
- end
- end
-
- return function(): string|nil
- local iter = iter_stack[#iter_stack]
- local path = path_stack[#path_stack]
- while true do
- local next_path, path_type = vim.loop.fs_scandir_next(iter)
-
- if not next_path then
- table.remove(iter_stack)
- table.remove(path_stack)
- if #iter_stack == 0 then
- return nil
- end
- iter = iter_stack[#iter_stack]
- path = path_stack[#path_stack]
- elseif
- (vim.startswith(next_path, '.') and not config.show_hidden)
- or (#config.ignore_pattern > 0 and string.find(next_path, config.ignore_pattern) ~= nil)
- then
- next_path = nil
- path_type = nil
- else
- local full_path = path .. os_sep .. next_path
- if path_type == 'directory' then
- if #iter_stack < config.max_depth then
- iter = vim.loop.fs_scandir(full_path)
- path = full_path
- table.insert(path_stack, full_path)
- table.insert(iter_stack, iter)
- end
- if not config.ignore_directories then
- return full_path
- end
- else
- return full_path
- end
- end
- end
- end
- end
-
- return cached('filepath', function(line_to_cursor: string, _: integer): {CompleteItem}, string
- local prefix = utils.prefix.lua_regex(os_path, line_to_cursor)
-
- local cwd = vim.fn.getcwd()
- local fpath: string
- local matches = {}
- for path in iter_files() do
- fpath = config.relative_paths and relpath(path, cwd) or path
-
- matches[#matches + 1] = {
- word = fpath,
- abbr = fpath,
- kind = '[path]',
- icase = 1,
- dup = 1,
- empty = 1,
- equal = 1,
- user_data = { source = 'filepath' },
- }
- end
-
- return matches, prefix
- end)
-end
-
--- Treesitter source
-
-local tslocals = require 'nvim-treesitter.locals'
-
-function Sources.treesitter_matches(opts: TreesitterOptions): Source
- local _config = options.get({} as TreesitterOptions, opts)
-
- return cached('treesitter', function(line_to_cursor: string, _lnum: integer): {CompleteItem}, string
- local prefix = utils.prefix.lua_regex('%S*$', line_to_cursor)
- local defs = tslocals.get_definitions(0)
-
- local items: {CompleteItem} = {}
-
- for _, def in ipairs(defs) do
- -- Determine kind and text
- local node: vim.treesitter.TSNode
- local kind: string
- for k,cap in pairs(def as {string:tslocals.LocalCapture.Captured}) do
- if k ~= 'associated' then
- node = cap.node
- kind = k
- break
- end
- end
-
- if node then
- items[#items + 1] = {
- word = vim.treesitter.get_node_text(node, 0),
- kind = kind,
- icase = 1,
- dup = 0,
- empty = 1,
- equal = 1,
- user_data = { source = 'treesitter' },
- }
- end
- end
-
- return items, prefix
- end)
-end
-
--- CompleteDone handlers
-
-local function lsp_completedone(completed_item: CompleteItem)
- local cursor = api.nvim_win_get_cursor(0)
- local col = cursor[2]
- local lnum = cursor[1] - 1
- local bufnr = api.nvim_get_current_buf()
-
- local extra = completed_item.user_data.extra as LspExtraInfo
- local item = extra.item
-
- local client = lsp.get_client_by_id(extra.client_id)
- if not client then
- error(string.format("Could not find client %d", extra.client_id))
- end
-
- local expand_snippet = item.insertTextFormat == 2
- local resolveEdits = (client.server_capabilities.completionProvider or {}).resolveProvider
- local offset_encoding = client and client.offset_encoding or 'utf-16'
-
- local tidy = function() end
- local suffix: string = nil
-
- if expand_snippet then
- local line = api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
- tidy = function()
- -- Remove the already inserted word
- local start_char = col - #completed_item.word
- local l = line
- api.nvim_buf_set_text(bufnr, lnum, start_char, lnum, #l, { '' })
- end
- suffix = line:sub(col + 1)
- end
-
- if item.additionalTextEdits then
- tidy()
- lsp.util.apply_text_edits(item.additionalTextEdits, bufnr, offset_encoding)
- if expand_snippet then
- apply_snippet(item, suffix, lnum)
- end
- elseif resolveEdits and type(item) == 'table' then
- local v = client.request_sync('completionItem/resolve', item, 1000, bufnr)
- assert(not v.err, vim.inspect(v.err))
- local res = v.result as lsp.LspCompletionItem
- if res.additionalTextEdits then
- tidy()
- tidy = function() end
- lsp.util.apply_text_edits(res.additionalTextEdits, bufnr, offset_encoding)
- end
- if expand_snippet then
- tidy()
- apply_snippet(item, suffix, lnum)
- end
- elseif expand_snippet then
- tidy()
- apply_snippet(item, suffix, lnum)
- end
-end
-
-local function luasnip_completedone(_: CompleteItem)
- if require('luasnip').expandable() then
- require('luasnip').expand()
- end
-end
-
-Sources.complete_done_cbs = {
- lsp = lsp_completedone,
- luasnip = luasnip_completedone,
-}
-
-return Sources
D teal/complementree/utils.tl => teal/complementree/utils.tl +0 -44
@@ 1,44 0,0 @@
-local api = vim.api
-
-local record Prefix
-end
-
-function Prefix.lua_regex(regex: string, line: string): string
- local pref_start = line:find(regex)
- local prefix = line:sub(pref_start)
-
- return prefix
-end
-
-function Prefix.vim_keyword(line: string): string
- local pref_start = vim.fn.match(line, '\\k*$') + 1
- local prefix = line:sub(pref_start)
-
- return prefix
-end
-
-local record Utils
- prefix: Prefix
-end
-
-function Utils.feed(codes: string)
- api.nvim_feedkeys(api.nvim_replace_termcodes(codes, true, true, true), 'm', true)
-end
-
-function Utils.cword(complete_item: CompleteItem): string
- return (complete_item.abbr or complete_item.word)
-end
-
-function Utils.make_relative_path(path: string, root: string): string
- if vim.startswith(path, root) then
- local baselen = #root
- if path:sub(0, baselen) == root then
- path = path:sub(baselen + 2)
- end
- end
- return path
-end
-
-Utils.prefix = Prefix
-
-return Utils