aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMathias Fussenegger <f.mathias@zignar.net>2023-10-21 09:47:24 +0200
committerMathias Fußenegger <mfussenegger@users.noreply.github.com>2023-10-21 13:49:05 +0200
commit1e10310f4cc70cf95a68457c2be9e7459b5bbba6 (patch)
treeba8fa01ace127ddb75554a66a1ff168bb778f8ce
parent9971bea6f1380c15c22f5035d1d33d994f8a6ed7 (diff)
downloadrneovim-1e10310f4cc70cf95a68457c2be9e7459b5bbba6.tar.gz
rneovim-1e10310f4cc70cf95a68457c2be9e7459b5bbba6.tar.bz2
rneovim-1e10310f4cc70cf95a68457c2be9e7459b5bbba6.zip
refactor(lsp): move completion logic into _completion module
To reduce cross-chatter between modules and for https://github.com/neovim/neovim/issues/25272 Also preparing for https://github.com/neovim/neovim/issues/25714
-rw-r--r--runtime/doc/lsp.txt4
-rw-r--r--runtime/lua/vim/lsp.lua95
-rw-r--r--runtime/lua/vim/lsp/_completion.lua210
-rw-r--r--runtime/lua/vim/lsp/protocol.lua1
-rw-r--r--runtime/lua/vim/lsp/util.lua93
5 files changed, 217 insertions, 186 deletions
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
index 8a0b144e83..3151a17417 100644
--- a/runtime/doc/lsp.txt
+++ b/runtime/doc/lsp.txt
@@ -1719,7 +1719,7 @@ extract_completion_items({result})
• {result} (table) The result of a `textDocument/completion` request
Return: ~
- (table) List of completion items
+ lsp.CompletionItem[] List of completion items
See also: ~
• https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
@@ -2014,7 +2014,7 @@ text_document_completion_list_to_complete_items({result}, {prefix})
• {prefix} (string) the prefix to filter the completion items
Return: ~
- (table) { matches = complete-items table, incomplete = bool }
+ table[] items
See also: ~
• complete-items
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 4b1da94d77..82a88772bd 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -2273,24 +2273,6 @@ function lsp.buf_notify(bufnr, method, params)
return resp
end
----@private
-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 - 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
- return util._str_byteindex_enc(line, min_start_char, encoding)
- else
- return nil
- end
-end
-
--- Implements 'omnifunc' compatible LSP completion.
---
---@see |complete-functions|
@@ -2307,82 +2289,7 @@ function lsp.omnifunc(findstart, base)
if log.debug() then
log.debug('omnifunc.findstart', { findstart = findstart, base = base })
end
-
- local bufnr = resolve_bufnr()
- 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
-
- -- Then, perform standard completion request
- if log.info() then
- log.info('base ', base)
- end
-
- local win = api.nvim_get_current_win()
- local pos = api.nvim_win_get_cursor(win)
- local line = api.nvim_get_current_line()
- local line_to_cursor = line:sub(1, pos[2])
- local _ = log.trace() and log.trace('omnifunc.line', pos, line)
-
- -- Get the start position of the current keyword
- local match_pos = vim.fn.match(line_to_cursor, '\\k*$') + 1
- local items = {}
-
- local startbyte
-
- local function on_done()
- local mode = api.nvim_get_mode()['mode']
- if mode == 'i' or mode == 'ic' then
- vim.fn.complete(startbyte or match_pos, items)
- end
- end
-
- 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
- log.warn(err.message)
- end
- if result and vim.fn.mode() == 'i' then
- -- Completion response items may be relative to a position different than `textMatch`.
- -- Concrete example, with sumneko/lua-language-server:
- --
- -- require('plenary.asy|
- -- ▲ ▲ ▲
- -- │ │ └── cursor_pos: 20
- -- │ └────── textMatch: 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 encoding = client.offset_encoding
- local candidates = util.extract_completion_items(result)
- local curstartbyte = adjust_start_col(pos[1], line, candidates, encoding)
- if startbyte == nil then
- startbyte = curstartbyte
- elseif curstartbyte ~= nil and curstartbyte ~= startbyte then
- startbyte = match_pos
- end
- local prefix = startbyte and line:sub(startbyte + 1) or line_to_cursor:sub(match_pos)
- local matches = util.text_document_completion_list_to_complete_items(result, prefix)
- 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
+ return require('vim.lsp._completion').omnifunc(findstart, base)
end
--- Provides an interface between the built-in client and a `formatexpr` function.
diff --git a/runtime/lua/vim/lsp/_completion.lua b/runtime/lua/vim/lsp/_completion.lua
new file mode 100644
index 0000000000..efd1aaacf7
--- /dev/null
+++ b/runtime/lua/vim/lsp/_completion.lua
@@ -0,0 +1,210 @@
+local M = {}
+local api = vim.api
+local lsp = vim.lsp
+local protocol = lsp.protocol
+local ms = protocol.Methods
+
+---@param input string unparsed snippet
+---@return string parsed snippet
+local function parse_snippet(input)
+ local ok, parsed = pcall(function()
+ return require('vim.lsp._snippet_grammar').parse(input)
+ end)
+ return ok and tostring(parsed) or input
+end
+
+--- Returns text that should be inserted when 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
+
+---@param result lsp.CompletionList|lsp.CompletionItem[]
+---@return lsp.CompletionItem[]
+local function get_items(result)
+ if result.items then
+ return result.items
+ end
+ return result
+end
+
+--- Turns the result of a `textDocument/completion` request into vim-compatible
+--- |complete-items|.
+---
+---@param result lsp.CompletionList|lsp.CompletionItem[] Result of `textDocument/completion`
+---@param prefix string prefix to filter the completion items
+---@return table[]
+---@see complete-items
+function M._lsp_to_complete_items(result, prefix)
+ 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 nil,
+ icase = 1,
+ dup = 1,
+ empty = 1,
+ user_data = {
+ nvim = {
+ lsp = {
+ completion_item = item,
+ },
+ },
+ },
+ })
+ end
+ return matches
+end
+
+---@param items lsp.CompletionItem[]
+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 - 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
+ return vim.lsp.util._str_byteindex_enc(line, min_start_char, encoding)
+ else
+ return nil
+ end
+end
+
+---@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)
+ 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 log = require('vim.lsp.log')
+ -- Then, perform standard completion request
+ if log.info() then
+ log.info('base ', base)
+ end
+
+ local win = api.nvim_get_current_win()
+ local pos = api.nvim_win_get_cursor(win)
+ local line = api.nvim_get_current_line()
+ local line_to_cursor = line:sub(1, pos[2])
+ log.trace('omnifunc.line', pos, line)
+
+ local word_boundary = vim.fn.match(line_to_cursor, '\\k*$') + 1 --[[@as integer]]
+ local items = {}
+ local startbyte = nil
+
+ local function on_done()
+ local mode = api.nvim_get_mode()['mode']
+ if mode == 'i' or mode == 'ic' then
+ vim.fn.complete(startbyte or word_boundary, 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
+ log.warn(err.message)
+ end
+ if result and vim.fn.mode() == 'i' then
+ -- Completion response items may be relative to a position different than `textMatch`.
+ -- Concrete example, with sumneko/lua-language-server:
+ --
+ -- require('plenary.asy|
+ -- ▲ ▲ ▲
+ -- │ │ └── cursor_pos: 20
+ -- │ └────── textMatch: 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 encoding = client.offset_encoding
+ local candidates = get_items(result)
+ local curstartbyte = adjust_start_col(pos[1], line, candidates, encoding)
+ if startbyte == nil then
+ startbyte = curstartbyte
+ elseif curstartbyte ~= nil and curstartbyte ~= startbyte then
+ startbyte = word_boundary
+ end
+ local prefix = startbyte and line:sub(startbyte + 1) or line_to_cursor:sub(word_boundary)
+ local matches = M._lsp_to_complete_items(result, prefix)
+ 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
+
+return M
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 3a1b16c450..7a48c800c6 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -242,6 +242,7 @@ local constants = {
-- Defines whether the insert text in a completion item should be interpreted as
-- plain text or a snippet.
+ --- @enum lsp.InsertTextFormat
InsertTextFormat = {
-- The primary text to be inserted is treated as a plain string.
PlainText = 1,
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 42c1508cbf..7ccb8a38b1 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -548,7 +548,7 @@ end
--- `textDocument/completion` request, which may return one of
--- `CompletionItem[]`, `CompletionList` or null.
---@param result table The result of a `textDocument/completion` request
----@return table List of completion items
+---@return lsp.CompletionItem[] List of completion items
---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
function M.extract_completion_items(result)
if type(result) == 'table' and result.items then
@@ -619,47 +619,6 @@ function M.parse_snippet(input)
return tostring(parsed)
end
---- Sorts by CompletionItem.sortText.
----
---see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
-local function sort_completion_items(items)
- table.sort(items, function(a, b)
- return (a.sortText or a.label) < (b.sortText or b.label)
- end)
-end
-
---- Returns text that should be inserted when 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
-local function get_completion_word(item)
- if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
- local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
- if insert_text_format == 'PlainText' or insert_text_format == nil then
- return item.textEdit.newText
- else
- return M.parse_snippet(item.textEdit.newText)
- end
- elseif item.insertText ~= nil and item.insertText ~= '' then
- local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
- if insert_text_format == 'PlainText' or insert_text_format == nil then
- return item.insertText
- else
- return M.parse_snippet(item.insertText)
- end
- end
- return item.label
-end
-
---- Some language servers return complementary candidates whose prefixes do not
---- match are also returned. So we exclude completion candidates whose prefix
---- does not match.
-local function remove_unmatch_completion_items(items, prefix)
- return vim.tbl_filter(function(item)
- local word = get_completion_word(item)
- return vim.startswith(word, prefix)
- end, items)
-end
-
--- According to LSP spec, if the client set `completionItemKind.valueSet`,
--- the client must handle it properly even if it receives a value outside the
--- specification.
@@ -678,56 +637,10 @@ end
--- from |vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
--- `CompletionList` or `null`
---@param prefix (string) the prefix to filter the completion items
----@return table { matches = complete-items table, incomplete = bool }
+---@return table[] items
---@see complete-items
function M.text_document_completion_list_to_complete_items(result, prefix)
- local items = M.extract_completion_items(result)
- if vim.tbl_isempty(items) then
- return {}
- end
-
- items = remove_unmatch_completion_items(items, prefix)
- sort_completion_items(items)
-
- local matches = {}
-
- for _, completion_item in ipairs(items) do
- local info = ''
- local documentation = completion_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(completion_item)
- table.insert(matches, {
- word = word,
- abbr = completion_item.label,
- kind = M._get_completion_item_kind_name(completion_item.kind),
- menu = completion_item.detail or '',
- info = #info > 0 and info or nil,
- icase = 1,
- dup = 1,
- empty = 1,
- user_data = {
- nvim = {
- lsp = {
- completion_item = completion_item,
- },
- },
- },
- })
- end
-
- return matches
+ return require('vim.lsp._completion')._lsp_to_complete_items(result, prefix)
end
--- Like vim.fn.bufwinid except it works across tabpages.