From ff097f2091e7a970e5b12960683b4dade5563040 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Sun, 4 Feb 2024 14:13:23 -0800 Subject: feat(lsp): completion side effects --- runtime/lua/vim/lsp/completion.lua | 734 +++++++++++++++++++++++++++++++++++++ 1 file changed, 734 insertions(+) create mode 100644 runtime/lua/vim/lsp/completion.lua (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua new file mode 100644 index 0000000000..25b3d53c8c --- /dev/null +++ b/runtime/lua/vim/lsp/completion.lua @@ -0,0 +1,734 @@ +local M = {} + +local api = vim.api +local lsp = vim.lsp +local protocol = lsp.protocol +local ms = protocol.Methods + +local rtt_ms = 50 +local ns_to_ms = 0.000001 + +--- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[] + +-- TODO(mariasolos): Remove this declaration once we figure out a better way to handle +-- literal/anonymous types (see https://github.com/neovim/neovim/pull/27542/files#r1495259331). +--- @nodoc +--- @class lsp.ItemDefaults +--- @field editRange lsp.Range | { insert: lsp.Range, replace: lsp.Range } | nil +--- @field insertTextFormat lsp.InsertTextFormat? +--- @field insertTextMode lsp.InsertTextMode? +--- @field data any + +--- @nodoc +--- @class vim.lsp.completion.BufHandle +--- @field clients table +--- @field triggers table + +--- @type table +local buf_handles = {} + +--- @nodoc +--- @class vim.lsp.completion.Context +local Context = { + cursor = nil, --- @type { [1]: integer, [2]: integer }? + last_request_time = nil, --- @type integer? + pending_requests = {}, --- @type function[] + isIncomplete = false, +} + +--- @nodoc +function Context:cancel_pending() + for _, cancel in ipairs(self.pending_requests) do + cancel() + end + + self.pending_requests = {} +end + +--- @nodoc +function Context:reset() + -- Note that the cursor isn't reset here, it needs to survive a `CompleteDone` event. + self.isIncomplete = false + self.last_request_time = nil + self:cancel_pending() +end + +--- @type uv.uv_timer_t? +local completion_timer = nil + +--- @return uv.uv_timer_t +local function new_timer() + return assert(vim.uv.new_timer()) +end + +local function reset_timer() + if completion_timer then + completion_timer:stop() + completion_timer:close() + end + + completion_timer = nil +end + +--- @param window integer +--- @param warmup integer +--- @return fun(sample: number): number +local function exp_avg(window, warmup) + local count = 0 + local sum = 0 + local value = 0 + + return function(sample) + if count < warmup then + count = count + 1 + sum = sum + sample + value = sum / count + else + local factor = 2.0 / (window + 1) + value = value * (1 - factor) + sample * factor + end + return value + end +end +local compute_new_average = exp_avg(10, 10) + +--- @return number +local function next_debounce() + if not Context.last_request_time then + return rtt_ms + end + + local ms_since_request = (vim.uv.hrtime() - Context.last_request_time) * ns_to_ms + return math.max((ms_since_request - rtt_ms) * -1, 0) +end + +--- @param input string Unparsed snippet +--- @return string # Parsed snippet if successful, else returns its input +local function parse_snippet(input) + local ok, parsed = pcall(function() + return lsp._snippet_grammar.parse(input) + end) + return ok and tostring(parsed) or input +end + +--- @param item lsp.CompletionItem +--- @param suffix? string +local function apply_snippet(item, suffix) + if item.textEdit then + vim.snippet.expand(item.textEdit.newText .. suffix) + elseif item.insertText then + vim.snippet.expand(item.insertText .. suffix) + end +end + +--- Returns text that should be inserted when a selecting completion item. The +--- precedence is as follows: textEdit.newText > insertText > label +--- +--- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +--- +--- @param item lsp.CompletionItem +--- @return string +local function get_completion_word(item) + if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then + if item.insertTextFormat == protocol.InsertTextFormat.PlainText then + return item.textEdit.newText + else + return parse_snippet(item.textEdit.newText) + end + elseif item.insertText ~= nil and item.insertText ~= '' then + if item.insertTextFormat == protocol.InsertTextFormat.PlainText then + return item.insertText + else + return parse_snippet(item.insertText) + end + end + return item.label +end + +--- Applies the given defaults to the completion item, modifying it in place. +--- +--- @param item lsp.CompletionItem +--- @param defaults lsp.ItemDefaults? +local function apply_defaults(item, defaults) + if not defaults then + return + end + + item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat + item.insertTextMode = item.insertTextMode or defaults.insertTextMode + item.data = item.data or defaults.data + if defaults.editRange then + local textEdit = item.textEdit or {} + item.textEdit = textEdit + textEdit.newText = textEdit.newText or item.textEditText or item.insertText + if defaults.editRange.start then + textEdit.range = textEdit.range or defaults.editRange + elseif defaults.editRange.insert then + textEdit.insert = defaults.editRange.insert + textEdit.replace = defaults.editRange.replace + end + end +end + +--- @param result vim.lsp.CompletionResult +--- @return lsp.CompletionItem[] +local function get_items(result) + if result.items then + -- When we have a list, apply the defaults and return an array of items. + for _, item in ipairs(result.items) do + ---@diagnostic disable-next-line: param-type-mismatch + apply_defaults(item, result.itemDefaults) + end + return result.items + else + -- Else just return the items as they are. + return result + end +end + +--- Turns the result of a `textDocument/completion` request into vim-compatible +--- |complete-items|. +--- +--- @private +--- @param result vim.lsp.CompletionResult Result of `textDocument/completion` +--- @param prefix string prefix to filter the completion items +--- @param client_id integer? Client ID +--- @return table[] +--- @see complete-items +function M._lsp_to_complete_items(result, prefix, client_id) + local items = get_items(result) + if vim.tbl_isempty(items) then + return {} + end + + local function matches_prefix(item) + return vim.startswith(get_completion_word(item), prefix) + end + + items = vim.tbl_filter(matches_prefix, items) --[[@as lsp.CompletionItem[]|]] + table.sort(items, function(a, b) + return (a.sortText or a.label) < (b.sortText or b.label) + end) + + local matches = {} + for _, item in ipairs(items) do + local info = '' + local documentation = item.documentation + if documentation then + if type(documentation) == 'string' and documentation ~= '' then + info = documentation + elseif type(documentation) == 'table' and type(documentation.value) == 'string' then + info = documentation.value + else + vim.notify( + ('invalid documentation value %s'):format(vim.inspect(documentation)), + vim.log.levels.WARN + ) + end + end + local word = get_completion_word(item) + table.insert(matches, { + word = word, + abbr = item.label, + kind = protocol.CompletionItemKind[item.kind] or 'Unknown', + menu = item.detail or '', + info = #info > 0 and info or '', + icase = 1, + dup = 1, + empty = 1, + user_data = { + nvim = { + lsp = { + completion_item = item, + client_id = client_id, + }, + }, + }, + }) + end + return matches +end + +--- @param lnum integer 0-indexed +--- @param line string +--- @param items lsp.CompletionItem[] +--- @param encoding string +--- @return integer? +local function adjust_start_col(lnum, line, items, encoding) + local min_start_char = nil + for _, item in pairs(items) do + if item.textEdit and item.textEdit.range.start.line == lnum 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 + return lsp.util._str_byteindex_enc(line, min_start_char, encoding) + else + return nil + end +end + +--- @private +--- @param line string line content +--- @param lnum integer 0-indexed line number +--- @param cursor_col integer +--- @param client_id integer client ID +--- @param client_start_boundary integer 0-indexed word boundary +--- @param server_start_boundary? integer 0-indexed word boundary, based on textEdit.range.start.character +--- @param result vim.lsp.CompletionResult +--- @param encoding string +--- @return table[] matches +--- @return integer? server_start_boundary +function M._convert_results( + line, + lnum, + cursor_col, + client_id, + client_start_boundary, + server_start_boundary, + result, + encoding +) + -- Completion response items may be relative to a position different than `client_start_boundary`. + -- Concrete example, with lua-language-server: + -- + -- require('plenary.asy| + -- ▲ ▲ ▲ + -- │ │ └── cursor_pos: 20 + -- │ └────── client_start_boundary: 17 + -- └────────────── textEdit.range.start.character: 9 + -- .newText = 'plenary.async' + -- ^^^ + -- prefix (We'd remove everything not starting with `asy`, + -- so we'd eliminate the `plenary.async` result + -- + -- `adjust_start_col` is used to prefer the language server boundary. + -- + local candidates = get_items(result) + local curstartbyte = adjust_start_col(lnum, line, candidates, encoding) + if server_start_boundary == nil then + server_start_boundary = curstartbyte + elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then + server_start_boundary = client_start_boundary + end + local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col) + local matches = M._lsp_to_complete_items(result, prefix, client_id) + return matches, server_start_boundary +end + +--- Implements 'omnifunc' compatible LSP completion. +--- +--- @see |complete-functions| +--- @see |complete-items| +--- @see |CompleteDone| +--- +--- @param findstart integer 0 or 1, decides behavior +--- @param base integer findstart=0, text to match against +--- +--- @return integer|table Decided by {findstart}: +--- - findstart=0: column where the completion starts, or -2 or -3 +--- - findstart=1: list of matches (actually just calls |complete()|) +function M._omnifunc(findstart, base) + vim.lsp.log.debug('omnifunc.findstart', { findstart = findstart, base = base }) + assert(base) -- silence luals + local bufnr = api.nvim_get_current_buf() + local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion }) + local remaining = #clients + if remaining == 0 then + return findstart == 1 and -1 or {} + end + + local win = api.nvim_get_current_win() + local cursor = api.nvim_win_get_cursor(win) + local lnum = cursor[1] - 1 + local cursor_col = cursor[2] + local line = api.nvim_get_current_line() + local line_to_cursor = line:sub(1, cursor_col) + local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$') --[[@as integer]] + local server_start_boundary = nil + local items = {} + + local function on_done() + local mode = api.nvim_get_mode()['mode'] + if mode == 'i' or mode == 'ic' then + vim.fn.complete((server_start_boundary or client_start_boundary) + 1, items) + end + end + + local util = vim.lsp.util + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + client.request(ms.textDocument_completion, params, function(err, result) + if err then + lsp.log.warn(err.message) + end + if result and vim.fn.mode() == 'i' then + local matches + matches, server_start_boundary = M._convert_results( + line, + lnum, + cursor_col, + client.id, + client_start_boundary, + server_start_boundary, + result, + client.offset_encoding + ) + vim.list_extend(items, matches) + end + remaining = remaining - 1 + if remaining == 0 then + vim.schedule(on_done) + end + end, bufnr) + end + + -- Return -2 to signal that we should continue completion so that we can + -- async complete. + return -2 +end + +--- @param clients table +--- @param bufnr integer +--- @param win integer +--- @param callback fun(responses: table) +--- @return function # Cancellation function +local function request(clients, bufnr, win, callback) + local responses = {} --- @type table + local request_ids = {} --- @type table + local remaining_requests = vim.tbl_count(clients) + + for client_id, client in pairs(clients) do + local params = lsp.util.make_position_params(win, client.offset_encoding) + local ok, request_id = client.request(ms.textDocument_completion, params, function(err, result) + responses[client_id] = { err = err, result = result } + remaining_requests = remaining_requests - 1 + if remaining_requests == 0 then + callback(responses) + end + end, bufnr) + + if ok then + request_ids[client_id] = request_id + end + end + + return function() + for client_id, request_id in pairs(request_ids) do + local client = lsp.get_client_by_id(client_id) + if client then + client.cancel_request(request_id) + end + end + end +end + +--- @param handle vim.lsp.completion.BufHandle +local function on_insert_char_pre(handle) + if tonumber(vim.fn.pumvisible()) == 1 then + if Context.isIncomplete then + reset_timer() + + local debounce_ms = next_debounce() + if debounce_ms == 0 then + vim.schedule(M.trigger) + else + completion_timer = new_timer() + completion_timer:start(debounce_ms, 0, vim.schedule_wrap(M.trigger)) + end + end + + return + end + + local char = api.nvim_get_vvar('char') + if not completion_timer and handle.triggers[char] then + completion_timer = assert(vim.uv.new_timer()) + completion_timer:start(25, 0, function() + reset_timer() + vim.schedule(M.trigger) + end) + end +end + +local function on_insert_leave() + reset_timer() + Context.cursor = nil + Context:reset() +end + +local function on_complete_done() + 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.nvim then + Context:reset() + return + end + + local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(0)) --- @type integer, integer + cursor_row = cursor_row - 1 + local completion_item = completed_item.user_data.nvim.lsp.completion_item --- @type lsp.CompletionItem + local client_id = completed_item.user_data.nvim.lsp.client_id --- @type integer + if not completion_item or not client_id then + Context:reset() + return + end + + local bufnr = api.nvim_get_current_buf() + local expand_snippet = completion_item.insertTextFormat == protocol.InsertTextFormat.Snippet + and (completion_item.textEdit ~= nil or completion_item.insertText ~= nil) + + Context:reset() + + local client = lsp.get_client_by_id(client_id) + if not client then + return + end + + local offset_encoding = client.offset_encoding or 'utf-16' + local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider + + local function clear_word() + if not expand_snippet then + return nil + end + + -- Remove the already inserted word. + local start_char = cursor_col - #completed_item.word + local line = api.nvim_buf_get_lines(bufnr, cursor_row, cursor_row + 1, true)[1] + api.nvim_buf_set_text(bufnr, cursor_row, start_char, cursor_row, #line, { '' }) + return line:sub(cursor_col + 1) + end + + --- @param suffix? string + local function apply_snippet_and_command(suffix) + if expand_snippet then + apply_snippet(completion_item, suffix) + end + + local command = completion_item.command + if command then + client:_exec_cmd(command, { bufnr = bufnr }, nil, function() + vim.lsp.log.warn( + string.format( + 'Language server `%s` does not support command `%s`. This command may require a client extension.', + client.name, + command.command + ) + ) + end) + end + end + + if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then + local suffix = clear_word() + lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, offset_encoding) + apply_snippet_and_command(suffix) + elseif resolve_provider and type(completion_item) == 'table' then + local changedtick = vim.b[bufnr].changedtick + + --- @param result lsp.CompletionItem + client.request(ms.completionItem_resolve, completion_item, function(err, result) + if changedtick ~= vim.b[bufnr].changedtick then + return + end + + local suffix = clear_word() + if err then + vim.notify_once(err.message, vim.log.levels.WARN) + elseif result and result.additionalTextEdits then + lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, offset_encoding) + if result.command then + completion_item.command = result.command + end + end + + apply_snippet_and_command(suffix) + end, bufnr) + else + local suffix = clear_word() + apply_snippet_and_command(suffix) + end +end + +--- @class vim.lsp.completion.BufferOpts +--- @field autotrigger? boolean Whether to trigger completion automatically. Default: false + +--- @param client_id integer +---@param bufnr integer +---@param opts vim.lsp.completion.BufferOpts +local function enable_completions(client_id, bufnr, opts) + if not buf_handles[bufnr] then + buf_handles[bufnr] = { clients = {}, triggers = {} } + + -- Attach to buffer events. + api.nvim_buf_attach(bufnr, false, { + on_detach = function(_, buf) + buf_handles[buf] = nil + end, + on_reload = function(_, buf) + M.enable(true, client_id, buf, opts) + end, + }) + + -- Set up autocommands. + local group = + api.nvim_create_augroup(string.format('vim/lsp/completion-%d', bufnr), { clear = true }) + api.nvim_create_autocmd('CompleteDone', { + group = group, + buffer = bufnr, + callback = function() + local reason = api.nvim_get_vvar('event').reason --- @type string + if reason == 'accept' then + on_complete_done() + end + end, + }) + if opts.autotrigger then + api.nvim_create_autocmd('InsertCharPre', { + group = group, + buffer = bufnr, + callback = function() + on_insert_char_pre(buf_handles[bufnr]) + end, + }) + api.nvim_create_autocmd('InsertLeave', { + group = group, + buffer = bufnr, + callback = on_insert_leave, + }) + end + end + + if not buf_handles[bufnr].clients[client_id] then + local client = lsp.get_client_by_id(client_id) + assert(client, 'invalid client ID') + + -- Add the new client to the buffer's clients. + buf_handles[bufnr].clients[client_id] = client + + -- Add the new client to the clients that should be triggered by its trigger characters. + --- @type string[] + local triggers = vim.tbl_get( + client.server_capabilities, + 'completionProvider', + 'triggerCharacters' + ) or {} + for _, char in ipairs(triggers) do + local clients_for_trigger = buf_handles[bufnr].triggers[char] + if not clients_for_trigger then + clients_for_trigger = {} + buf_handles[bufnr].triggers[char] = clients_for_trigger + end + local client_exists = vim.iter(clients_for_trigger):any(function(c) + return c.id == client_id + end) + if not client_exists then + table.insert(clients_for_trigger, client) + end + end + end +end + +--- @param client_id integer +--- @param bufnr integer +local function disable_completions(client_id, bufnr) + local handle = buf_handles[bufnr] + if not handle then + return + end + + handle.clients[client_id] = nil + if not next(handle.clients) then + buf_handles[bufnr] = nil + api.nvim_del_augroup_by_name(string.format('vim/lsp/completion-%d', bufnr)) + else + for char, clients in pairs(handle.triggers) do + --- @param c vim.lsp.Client + handle.triggers[char] = vim.tbl_filter(function(c) + return c.id ~= client_id + end, clients) + end + end +end + +--- Enables or disables completions from the given language client in the given buffer. +--- +--- @param enable boolean True to enable, false to disable +--- @param client_id integer Client ID +--- @param bufnr integer Buffer handle, or 0 for the current buffer +--- @param opts? vim.lsp.completion.BufferOpts +function M.enable(enable, client_id, bufnr, opts) + bufnr = (bufnr == 0 and api.nvim_get_current_buf()) or bufnr + + if enable then + enable_completions(client_id, bufnr, opts or {}) + else + disable_completions(client_id, bufnr) + end +end + +--- Trigger LSP completion in the current buffer. +function M.trigger() + reset_timer() + Context:cancel_pending() + + local win = api.nvim_get_current_win() + local bufnr = api.nvim_get_current_buf() + local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer + local line = api.nvim_get_current_line() + local line_to_cursor = line:sub(1, cursor_col) + local clients = (buf_handles[bufnr] or {}).clients or {} + local word_boundary = vim.fn.match(line_to_cursor, '\\k*$') + local start_time = vim.uv.hrtime() + Context.last_request_time = start_time + + local cancel_request = request(clients, bufnr, win, function(responses) + local end_time = vim.uv.hrtime() + rtt_ms = compute_new_average((end_time - start_time) * ns_to_ms) + + Context.pending_requests = {} + Context.isIncomplete = false + + local row_changed = api.nvim_win_get_cursor(win)[1] ~= cursor_row + local mode = api.nvim_get_mode().mode + if row_changed or not (mode == 'i' or mode == 'ic') then + return + end + + local matches = {} + local server_start_boundary --- @type integer? + for client_id, response in pairs(responses) do + if response.err then + vim.notify_once(response.err.message, vim.log.levels.warn) + end + + local result = response.result + if result then + Context.isIncomplete = Context.isIncomplete or result.isIncomplete + local client = lsp.get_client_by_id(client_id) + local encoding = client and client.offset_encoding or 'utf-16' + local client_matches + client_matches, server_start_boundary = M._convert_results( + line, + cursor_row - 1, + cursor_col, + client_id, + word_boundary, + nil, + result, + encoding + ) + vim.list_extend(matches, client_matches) + end + end + local start_col = (server_start_boundary or word_boundary) + 1 + vim.fn.complete(start_col, matches) + end) + + table.insert(Context.pending_requests, cancel_request) +end + +return M -- cgit From 0df2c6b5d09fab392dd1a14e4b2e6a3b03203aaa Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Tue, 28 May 2024 21:37:46 +0200 Subject: feat(lsp): use fuzzy match on filterText instead of prefix match The `complete()` mechanism matches completion candidates against the typed text, so strict pre-filtering isn't necessary. This is a first step towards supporting postfix snippets (like `items@insert` in luals) --- runtime/lua/vim/lsp/completion.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 25b3d53c8c..f9b0563b86 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -201,11 +201,17 @@ function M._lsp_to_complete_items(result, prefix, client_id) return {} end - local function matches_prefix(item) - return vim.startswith(get_completion_word(item), prefix) - end + if prefix ~= '' then + ---@param item lsp.CompletionItem + local function match_prefix(item) + if item.filterText then + return next(vim.fn.matchfuzzy({ item.filterText }, prefix)) + end + return true + end - items = vim.tbl_filter(matches_prefix, items) --[[@as lsp.CompletionItem[]|]] + items = vim.tbl_filter(match_prefix, items) --[[@as lsp.CompletionItem[]|]] + end table.sort(items, function(a, b) return (a.sortText or a.label) < (b.sortText or b.label) end) -- cgit From b2bad0ac91ddb9b33c3547b6fd4f7278794818d9 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Tue, 28 May 2024 23:20:25 +0200 Subject: feat(lsp): support postfix snippets in completion --- runtime/lua/vim/lsp/completion.lua | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index f9b0563b86..39c0c5fa29 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -129,18 +129,33 @@ end --- @param item lsp.CompletionItem --- @return string local function get_completion_word(item) - if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then - if item.insertTextFormat == protocol.InsertTextFormat.PlainText then - return item.textEdit.newText - else - return parse_snippet(item.textEdit.newText) - end - elseif item.insertText ~= nil and item.insertText ~= '' then - if item.insertTextFormat == protocol.InsertTextFormat.PlainText then - return item.insertText - else + if item.insertTextFormat == protocol.InsertTextFormat.Snippet then + if item.textEdit then + -- Use label instead of text if text has different starting characters. + -- label is used as abbr (=displayed), but word is used for filtering + -- This is required for things like postfix completion. + -- E.g. in lua: + -- + -- local f = {} + -- f@| + -- ▲ + -- └─ cursor + -- + -- item.textEdit.newText: table.insert(f, $0) + -- label: insert + -- + -- Typing `i` would remove the candidate because newText starts with `t`. + local text = item.insertText or item.textEdit.newText + return #text < #item.label and text or item.label + elseif item.insertText and item.insertText ~= '' then return parse_snippet(item.insertText) + else + return item.label end + elseif item.textEdit then + return item.textEdit.newText + elseif item.insertText and item.insertText ~= '' then + return item.insertText end return item.label end -- cgit From cc1f2d2ca6caa3da4cb7910f74fed4375202e888 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Thu, 30 May 2024 10:24:54 +0200 Subject: perf(lsp): don't copy completion items in filter pass --- runtime/lua/vim/lsp/completion.lua | 100 ++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 46 deletions(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 39c0c5fa29..8777947acf 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -201,6 +201,24 @@ local function get_items(result) end end +---@param item lsp.CompletionItem +---@return string +local function get_doc(item) + local doc = item.documentation + if not doc then + return '' + end + if type(doc) == 'string' then + return doc + end + if type(doc) == 'table' and type(doc.value) == 'string' then + return doc.value + end + + vim.notify('invalid documentation value: ' .. vim.inspect(doc), vim.log.levels.WARN) + return '' +end + --- Turns the result of a `textDocument/completion` request into vim-compatible --- |complete-items|. --- @@ -216,58 +234,48 @@ function M._lsp_to_complete_items(result, prefix, client_id) return {} end - if prefix ~= '' then - ---@param item lsp.CompletionItem - local function match_prefix(item) - if item.filterText then - return next(vim.fn.matchfuzzy({ item.filterText }, prefix)) - end - return true + local matches = prefix == '' and function() + return true + end or function(item) + if item.filterText then + return next(vim.fn.matchfuzzy({ item.filterText }, prefix)) end - - items = vim.tbl_filter(match_prefix, items) --[[@as lsp.CompletionItem[]|]] + return true end - table.sort(items, function(a, b) - return (a.sortText or a.label) < (b.sortText or b.label) - end) - - local matches = {} + local candidates = {} for _, item in ipairs(items) do - local info = '' - local documentation = item.documentation - if documentation then - if type(documentation) == 'string' and documentation ~= '' then - info = documentation - elseif type(documentation) == 'table' and type(documentation.value) == 'string' then - info = documentation.value - else - vim.notify( - ('invalid documentation value %s'):format(vim.inspect(documentation)), - vim.log.levels.WARN - ) - end - end - local word = get_completion_word(item) - table.insert(matches, { - word = word, - abbr = item.label, - kind = protocol.CompletionItemKind[item.kind] or 'Unknown', - menu = item.detail or '', - info = #info > 0 and info or '', - icase = 1, - dup = 1, - empty = 1, - user_data = { - nvim = { - lsp = { - completion_item = item, - client_id = client_id, + if matches(item) then + local word = get_completion_word(item) + table.insert(candidates, { + word = word, + abbr = item.label, + kind = protocol.CompletionItemKind[item.kind] or 'Unknown', + menu = item.detail or '', + info = get_doc(item), + icase = 1, + dup = 1, + empty = 1, + user_data = { + nvim = { + lsp = { + completion_item = item, + client_id = client_id, + }, }, }, - }, - }) + }) + end end - return matches + ---@diagnostic disable-next-line: no-unknown + table.sort(candidates, function(a, b) + ---@type lsp.CompletionItem + local itema = a.user_data.nvim.lsp.completion_item + ---@type lsp.CompletionItem + local itemb = b.user_data.nvim.lsp.completion_item + return (itema.sortText or itema.label) < (itemb.sortText or itemb.label) + end) + + return candidates end --- @param lnum integer 0-indexed -- cgit From 4c938f6d72710507db22074951eee23869ed49e0 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Thu, 30 May 2024 10:46:47 +0200 Subject: refactor(lsp): share completion request logic between omnifunc & trigger --- runtime/lua/vim/lsp/completion.lua | 209 +++++++++++++++---------------------- 1 file changed, 85 insertions(+), 124 deletions(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 8777947acf..c514c5ef4d 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -348,78 +348,6 @@ function M._convert_results( return matches, server_start_boundary end ---- Implements 'omnifunc' compatible LSP completion. ---- ---- @see |complete-functions| ---- @see |complete-items| ---- @see |CompleteDone| ---- ---- @param findstart integer 0 or 1, decides behavior ---- @param base integer findstart=0, text to match against ---- ---- @return integer|table Decided by {findstart}: ---- - findstart=0: column where the completion starts, or -2 or -3 ---- - findstart=1: list of matches (actually just calls |complete()|) -function M._omnifunc(findstart, base) - vim.lsp.log.debug('omnifunc.findstart', { findstart = findstart, base = base }) - assert(base) -- silence luals - local bufnr = api.nvim_get_current_buf() - local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion }) - local remaining = #clients - if remaining == 0 then - return findstart == 1 and -1 or {} - end - - local win = api.nvim_get_current_win() - local cursor = api.nvim_win_get_cursor(win) - local lnum = cursor[1] - 1 - local cursor_col = cursor[2] - local line = api.nvim_get_current_line() - local line_to_cursor = line:sub(1, cursor_col) - local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$') --[[@as integer]] - local server_start_boundary = nil - local items = {} - - local function on_done() - local mode = api.nvim_get_mode()['mode'] - if mode == 'i' or mode == 'ic' then - vim.fn.complete((server_start_boundary or client_start_boundary) + 1, items) - end - end - - local util = vim.lsp.util - for _, client in ipairs(clients) do - local params = util.make_position_params(win, client.offset_encoding) - client.request(ms.textDocument_completion, params, function(err, result) - if err then - lsp.log.warn(err.message) - end - if result and vim.fn.mode() == 'i' then - local matches - matches, server_start_boundary = M._convert_results( - line, - lnum, - cursor_col, - client.id, - client_start_boundary, - server_start_boundary, - result, - client.offset_encoding - ) - vim.list_extend(items, matches) - end - remaining = remaining - 1 - if remaining == 0 then - vim.schedule(on_done) - end - end, bufnr) - end - - -- Return -2 to signal that we should continue completion so that we can - -- async complete. - return -2 -end - --- @param clients table --- @param bufnr integer --- @param win integer @@ -455,6 +383,64 @@ local function request(clients, bufnr, win, callback) end end +local function trigger(bufnr, clients) + reset_timer() + Context:cancel_pending() + + local win = api.nvim_get_current_win() + local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer + local line = api.nvim_get_current_line() + local line_to_cursor = line:sub(1, cursor_col) + local word_boundary = vim.fn.match(line_to_cursor, '\\k*$') + local start_time = vim.uv.hrtime() + Context.last_request_time = start_time + + local cancel_request = request(clients, bufnr, win, function(responses) + local end_time = vim.uv.hrtime() + rtt_ms = compute_new_average((end_time - start_time) * ns_to_ms) + + Context.pending_requests = {} + Context.isIncomplete = false + + local row_changed = api.nvim_win_get_cursor(win)[1] ~= cursor_row + local mode = api.nvim_get_mode().mode + if row_changed or not (mode == 'i' or mode == 'ic') then + return + end + + local matches = {} + local server_start_boundary --- @type integer? + for client_id, response in pairs(responses) do + if response.err then + vim.notify_once(response.err.message, vim.log.levels.warn) + end + + local result = response.result + if result then + Context.isIncomplete = Context.isIncomplete or result.isIncomplete + local client = lsp.get_client_by_id(client_id) + local encoding = client and client.offset_encoding or 'utf-16' + local client_matches + client_matches, server_start_boundary = M._convert_results( + line, + cursor_row - 1, + cursor_col, + client_id, + word_boundary, + nil, + result, + encoding + ) + vim.list_extend(matches, client_matches) + end + end + local start_col = (server_start_boundary or word_boundary) + 1 + vim.fn.complete(start_col, matches) + end) + + table.insert(Context.pending_requests, cancel_request) +end + --- @param handle vim.lsp.completion.BufHandle local function on_insert_char_pre(handle) if tonumber(vim.fn.pumvisible()) == 1 then @@ -701,63 +687,38 @@ end --- Trigger LSP completion in the current buffer. function M.trigger() - reset_timer() - Context:cancel_pending() - - local win = api.nvim_get_current_win() local bufnr = api.nvim_get_current_buf() - local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer - local line = api.nvim_get_current_line() - local line_to_cursor = line:sub(1, cursor_col) local clients = (buf_handles[bufnr] or {}).clients or {} - local word_boundary = vim.fn.match(line_to_cursor, '\\k*$') - local start_time = vim.uv.hrtime() - Context.last_request_time = start_time - - local cancel_request = request(clients, bufnr, win, function(responses) - local end_time = vim.uv.hrtime() - rtt_ms = compute_new_average((end_time - start_time) * ns_to_ms) - - Context.pending_requests = {} - Context.isIncomplete = false - - local row_changed = api.nvim_win_get_cursor(win)[1] ~= cursor_row - local mode = api.nvim_get_mode().mode - if row_changed or not (mode == 'i' or mode == 'ic') then - return - end + trigger(bufnr, clients) +end - local matches = {} - local server_start_boundary --- @type integer? - for client_id, response in pairs(responses) do - if response.err then - vim.notify_once(response.err.message, vim.log.levels.warn) - end +--- Implements 'omnifunc' compatible LSP completion. +--- +--- @see |complete-functions| +--- @see |complete-items| +--- @see |CompleteDone| +--- +--- @param findstart integer 0 or 1, decides behavior +--- @param base integer findstart=0, text to match against +--- +--- @return integer|table Decided by {findstart}: +--- - findstart=0: column where the completion starts, or -2 or -3 +--- - findstart=1: list of matches (actually just calls |complete()|) +function M._omnifunc(findstart, base) + vim.lsp.log.debug('omnifunc.findstart', { findstart = findstart, base = base }) + assert(base) -- silence luals + local bufnr = api.nvim_get_current_buf() + local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion }) + local remaining = #clients + if remaining == 0 then + return findstart == 1 and -1 or {} + end - local result = response.result - if result then - Context.isIncomplete = Context.isIncomplete or result.isIncomplete - local client = lsp.get_client_by_id(client_id) - local encoding = client and client.offset_encoding or 'utf-16' - local client_matches - client_matches, server_start_boundary = M._convert_results( - line, - cursor_row - 1, - cursor_col, - client_id, - word_boundary, - nil, - result, - encoding - ) - vim.list_extend(matches, client_matches) - end - end - local start_col = (server_start_boundary or word_boundary) + 1 - vim.fn.complete(start_col, matches) - end) + trigger(bufnr, clients) - table.insert(Context.pending_requests, cancel_request) + -- Return -2 to signal that we should continue completion so that we can + -- async complete. + return -2 end return M -- cgit From 138a93a057dabd70673b7466bf5af41bd66f1385 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Thu, 30 May 2024 10:51:52 +0200 Subject: perf(lsp): avoid repeated table lookup in completion.enable --- runtime/lua/vim/lsp/completion.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index c514c5ef4d..b77bf3e3c2 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -575,8 +575,10 @@ end ---@param bufnr integer ---@param opts vim.lsp.completion.BufferOpts local function enable_completions(client_id, bufnr, opts) - if not buf_handles[bufnr] then - buf_handles[bufnr] = { clients = {}, triggers = {} } + local buf_handle = buf_handles[bufnr] + if not buf_handle then + buf_handle = { clients = {}, triggers = {} } + buf_handles[bufnr] = buf_handle -- Attach to buffer events. api.nvim_buf_attach(bufnr, false, { @@ -617,12 +619,12 @@ local function enable_completions(client_id, bufnr, opts) end end - if not buf_handles[bufnr].clients[client_id] then + if not buf_handle.clients[client_id] then local client = lsp.get_client_by_id(client_id) assert(client, 'invalid client ID') -- Add the new client to the buffer's clients. - buf_handles[bufnr].clients[client_id] = client + buf_handle.clients[client_id] = client -- Add the new client to the clients that should be triggered by its trigger characters. --- @type string[] @@ -632,10 +634,10 @@ local function enable_completions(client_id, bufnr, opts) 'triggerCharacters' ) or {} for _, char in ipairs(triggers) do - local clients_for_trigger = buf_handles[bufnr].triggers[char] + local clients_for_trigger = buf_handle.triggers[char] if not clients_for_trigger then clients_for_trigger = {} - buf_handles[bufnr].triggers[char] = clients_for_trigger + buf_handle.triggers[char] = clients_for_trigger end local client_exists = vim.iter(clients_for_trigger):any(function(c) return c.id == client_id -- cgit From 19be3d26830ced203631045f2f622e75e6d857a7 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Sun, 2 Jun 2024 09:54:15 +0200 Subject: fix(lsp): trim trailing whitespace from completion words (#29122) the `complete()` mechanism doesn't play nicely with trailing newlines or tabs. A newline causes it to insert a null character, showing up as `^@`. --- runtime/lua/vim/lsp/completion.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index b77bf3e3c2..f8e17ae2f0 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -153,7 +153,8 @@ local function get_completion_word(item) return item.label end elseif item.textEdit then - return item.textEdit.newText + local word = item.textEdit.newText + return word:match('^(%S*)') or word elseif item.insertText and item.insertText ~= '' then return item.insertText end -- cgit From 5aa9906676f3ad040b0ccb75eb5fd560def1e0ec Mon Sep 17 00:00:00 2001 From: ippachi Date: Tue, 4 Jun 2024 01:07:09 +0900 Subject: fix(lsp): use client.id instead of pairs index (#29143) Problem: Completion side effects not working randomly. Solution: When creating the table of LSP responses, the table index was used, but this is not the same as the actual client_id, so it was changed to use the client_id directly. --- runtime/lua/vim/lsp/completion.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index f8e17ae2f0..c9326a0681 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -349,7 +349,7 @@ function M._convert_results( return matches, server_start_boundary end ---- @param clients table +--- @param clients table # keys != client_id --- @param bufnr integer --- @param win integer --- @param callback fun(responses: table) @@ -359,7 +359,8 @@ local function request(clients, bufnr, win, callback) local request_ids = {} --- @type table local remaining_requests = vim.tbl_count(clients) - for client_id, client in pairs(clients) do + for _, client in pairs(clients) do + local client_id = client.id local params = lsp.util.make_position_params(win, client.offset_encoding) local ok, request_id = client.request(ms.textDocument_completion, params, function(err, result) responses[client_id] = { err = err, result = result } -- cgit From 8cbb1f20e557461c8417583a7f69d53aaaef920b Mon Sep 17 00:00:00 2001 From: Ilia Choly Date: Tue, 4 Jun 2024 09:06:02 -0400 Subject: refactor(lua): use tuple syntax everywhere #29111 --- runtime/lua/vim/lsp/completion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index c9326a0681..4b7deabf41 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -30,7 +30,7 @@ local buf_handles = {} --- @nodoc --- @class vim.lsp.completion.Context local Context = { - cursor = nil, --- @type { [1]: integer, [2]: integer }? + cursor = nil, --- @type [integer, integer]? last_request_time = nil, --- @type integer? pending_requests = {}, --- @type function[] isIncomplete = false, -- cgit From aa47af7e69bb32c4486510dce27f45d9028e0a6c Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Fri, 14 Jun 2024 19:32:34 +0200 Subject: fix(lsp): tune completion word extraction for decorated labels (#29331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: For snippets lsp.completion prefers the label if it is shorter than the insertText or textEdit to support postfix completion cases but clangd adds decoration characters to labels. E.g.: `•INT16_C(c)` Solution: Use parse_snippet on insertText/textEdit before checking if it is shorter than the label. Fixes https://github.com/neovim/neovim/issues/29301 --- runtime/lua/vim/lsp/completion.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 4b7deabf41..2e6d82b367 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -145,8 +145,8 @@ local function get_completion_word(item) -- label: insert -- -- Typing `i` would remove the candidate because newText starts with `t`. - local text = item.insertText or item.textEdit.newText - return #text < #item.label and text or item.label + local text = parse_snippet(item.insertText or item.textEdit.newText) + return #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label elseif item.insertText and item.insertText ~= '' then return parse_snippet(item.insertText) else -- cgit From 724d1110b1e4699a34f489e9cdb2d25098746499 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 27 Jun 2024 12:20:00 +0200 Subject: fix(lsp): pre-filter matches on label if filterText is missing (#29491) Although the built-in pum completion mechanism will filter anyway on the next input it is odd if the initial popup shows entries which don't match the current prefix. Using fuzzy match on the label/prefix is compatible with `completeopt+=fuzzy` and also doesn't seem to break postfix snippet cases Closes https://github.com/neovim/neovim/issues/29287 --- runtime/lua/vim/lsp/completion.lua | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 2e6d82b367..b935c48d3c 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -235,14 +235,20 @@ function M._lsp_to_complete_items(result, prefix, client_id) return {} end - local matches = prefix == '' and function() - return true - end or function(item) - if item.filterText then - return next(vim.fn.matchfuzzy({ item.filterText }, prefix)) + ---@type fun(item: lsp.CompletionItem):boolean + local matches + if prefix == '' then + matches = function(_) + return true + end + else + ---@param item lsp.CompletionItem + matches = function(item) + local text = item.filterText or item.label + return next(vim.fn.matchfuzzy({ text }, prefix)) ~= nil end - return true end + local candidates = {} for _, item in ipairs(items) do if matches(item) then -- cgit From aec7f1979ada1b34cfb3d8fd33769232d0323ea8 Mon Sep 17 00:00:00 2001 From: Sebastian Lyng Johansen Date: Tue, 2 Jul 2024 18:27:51 +0200 Subject: fix(lsp): fallback to `label` for completion items if all others are missing (#29522) --- runtime/lua/vim/lsp/completion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index b935c48d3c..1078e3eb7e 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -176,7 +176,7 @@ local function apply_defaults(item, defaults) if defaults.editRange then local textEdit = item.textEdit or {} item.textEdit = textEdit - textEdit.newText = textEdit.newText or item.textEditText or item.insertText + textEdit.newText = textEdit.newText or item.textEditText or item.insertText or item.label if defaults.editRange.start then textEdit.range = textEdit.range or defaults.editRange elseif defaults.editRange.insert then -- cgit From 4e90bc30237ae81bf15e77c17ac8089a2cc74046 Mon Sep 17 00:00:00 2001 From: glepnir Date: Wed, 31 Jul 2024 22:15:34 +0800 Subject: feat(lsp): lsp.completion support set deprecated (#29882) Problem: CompletionItem in lsp spec mentioned the deprecated attribute Solution: when item has deprecated attribute set hl_group to DiagnosticDeprecated in complete function --- runtime/lua/vim/lsp/completion.lua | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 1078e3eb7e..6a6659fd25 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -253,6 +253,13 @@ function M._lsp_to_complete_items(result, prefix, client_id) for _, item in ipairs(items) do if matches(item) then local word = get_completion_word(item) + local hl_group = '' + if + item.deprecated + or vim.list_contains((item.tags or {}), protocol.CompletionTag.Deprecated) + then + hl_group = 'DiagnosticDeprecated' + end table.insert(candidates, { word = word, abbr = item.label, @@ -262,6 +269,7 @@ function M._lsp_to_complete_items(result, prefix, client_id) icase = 1, dup = 1, empty = 1, + hl_group = hl_group, user_data = { nvim = { lsp = { -- cgit From e48179f31e6503bfa86bf08538e64456e96446a8 Mon Sep 17 00:00:00 2001 From: glepnir Date: Thu, 22 Aug 2024 15:51:44 +0800 Subject: fix(lsp): suppress completion request if completion is active (#30028) Problem: the autotrigger mechanism could fire completion requests despite completion already being active from another completion mechanism or manual trigger Solution: add a condition to avoid an additional request. --- runtime/lua/vim/lsp/completion.lua | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 6a6659fd25..022edf5fab 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -403,6 +403,10 @@ local function trigger(bufnr, clients) reset_timer() Context:cancel_pending() + if tonumber(vim.fn.pumvisible()) == 1 and Context.isIncomplete then + return + end + local win = api.nvim_get_current_win() local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer local line = api.nvim_get_current_line() -- cgit From 1f5bcc7c4ed7a68ae4e23933aee04c50b4df8bb5 Mon Sep 17 00:00:00 2001 From: glepnir Date: Fri, 23 Aug 2024 03:42:27 +0800 Subject: feat(lsp): completion opts support custom item conversion (#30060) Problem: Some items of completion results include function signatures that can cause the pum to be very long when a function has many params, because pum scales with the longest word/abbr. Solution: add custom covert function that can customise abbr to remove params. --- runtime/lua/vim/lsp/completion.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 022edf5fab..89d9a0e9b1 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -23,6 +23,7 @@ local ns_to_ms = 0.000001 --- @class vim.lsp.completion.BufHandle --- @field clients table --- @field triggers table +--- @field convert? fun(item: lsp.CompletionItem): table --- @type table local buf_handles = {} @@ -250,6 +251,8 @@ function M._lsp_to_complete_items(result, prefix, client_id) end local candidates = {} + local bufnr = api.nvim_get_current_buf() + local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert') for _, item in ipairs(items) do if matches(item) then local word = get_completion_word(item) @@ -260,7 +263,7 @@ function M._lsp_to_complete_items(result, prefix, client_id) then hl_group = 'DiagnosticDeprecated' end - table.insert(candidates, { + local completion_item = { word = word, abbr = item.label, kind = protocol.CompletionItemKind[item.kind] or 'Unknown', @@ -278,7 +281,11 @@ function M._lsp_to_complete_items(result, prefix, client_id) }, }, }, - }) + } + if user_convert then + completion_item = vim.tbl_extend('keep', user_convert(item), completion_item) + end + table.insert(candidates, completion_item) end end ---@diagnostic disable-next-line: no-unknown @@ -590,14 +597,15 @@ end --- @class vim.lsp.completion.BufferOpts --- @field autotrigger? boolean Whether to trigger completion automatically. Default: false +--- @field convert? fun(item: lsp.CompletionItem): table An optional function used to customize the transformation of an LSP CompletionItem to |complete-items|. ---- @param client_id integer +---@param client_id integer ---@param bufnr integer ---@param opts vim.lsp.completion.BufferOpts local function enable_completions(client_id, bufnr, opts) local buf_handle = buf_handles[bufnr] if not buf_handle then - buf_handle = { clients = {}, triggers = {} } + buf_handle = { clients = {}, triggers = {}, convert = opts.convert } buf_handles[bufnr] = buf_handle -- Attach to buffer events. -- cgit From 983953858e5610b104d35725c7da1e9025f60421 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Mon, 26 Aug 2024 17:34:54 +0200 Subject: fix(lsp): fix isIncomplete condition in completion trigger (#30130) Follow up to https://github.com/neovim/neovim/pull/30028#discussion_r1726539370 --- runtime/lua/vim/lsp/completion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 89d9a0e9b1..e28fec5e25 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -410,7 +410,7 @@ local function trigger(bufnr, clients) reset_timer() Context:cancel_pending() - if tonumber(vim.fn.pumvisible()) == 1 and Context.isIncomplete then + if tonumber(vim.fn.pumvisible()) == 1 and not Context.isIncomplete then return end -- cgit From 42ed0ffad9851f3794a9dff080a2789c87c6d7c8 Mon Sep 17 00:00:00 2001 From: glepnir Date: Sat, 31 Aug 2024 02:23:49 +0800 Subject: fix(lsp): when prefix is non word add all result into matches (#30044) Problem: prefix can be a symbol like period, the fuzzy matching can't handle it correctly. Solution: when prefix is empty or a symbol add all lsp completion result into matches. --- runtime/lua/vim/lsp/completion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index e28fec5e25..89d6d0e8b9 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -238,7 +238,7 @@ function M._lsp_to_complete_items(result, prefix, client_id) ---@type fun(item: lsp.CompletionItem):boolean local matches - if prefix == '' then + if not prefix:find('%w') then matches = function(_) return true end -- cgit From 61e9137394fc5229e582a64316c2ffef55d8d7af Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 1 Sep 2024 13:01:24 -0700 Subject: docs: misc #28970 --- runtime/lua/vim/lsp/completion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/completion.lua') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 89d6d0e8b9..71ea2df100 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -597,7 +597,7 @@ end --- @class vim.lsp.completion.BufferOpts --- @field autotrigger? boolean Whether to trigger completion automatically. Default: false ---- @field convert? fun(item: lsp.CompletionItem): table An optional function used to customize the transformation of an LSP CompletionItem to |complete-items|. +--- @field convert? fun(item: lsp.CompletionItem): table Transforms an LSP CompletionItem to |complete-items|. ---@param client_id integer ---@param bufnr integer -- cgit