neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit b9e6fa7ec81c463d77cc919392b52f6df2d8d304
parent 3530182ba491ba8663b40bdff0c044d74e89bb82
Author: Mathias Fussenegger <f.mathias@zignar.net>
Date:   Fri, 17 Jan 2025 15:27:50 +0100

fix(lsp): use filterText as word if textEdit/label doesn't match

Problem:

With language servers like lemminx, completing xml tags like `<mo` first
shows the right candidates (`modules`) but after typing `d` the
candidates disappear.

This is because the server returns:

    [...]
    filterText = "<module",
    label = "module",
    textEdit = {
      newText = "<module>$1</module>$0",

Which resulted in `module` being used as `word`, and `module` doesn't
match the prefix `<mo`. Typing `d` causes the `complete()` filtering
mechanism to kick in and remove the entry.

Solution:

Use `<module` from the `filterText` as `word` if the textEdit/label
heuristic doesn't match.

Diffstat:
Mruntime/lua/vim/lsp/completion.lua | 13++++++++++---
Mtest/functional/plugin/lsp/completion_spec.lua | 32++++++++++++++++++++++++++++++++
2 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua @@ -127,8 +127,10 @@ end --- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion --- --- @param item lsp.CompletionItem +--- @param prefix string +--- @param match fun(text: string, prefix: string):boolean --- @return string -local function get_completion_word(item) +local function get_completion_word(item, prefix, match) if item.insertTextFormat == protocol.InsertTextFormat.Snippet then if item.textEdit then -- Use label instead of text if text has different starting characters. @@ -146,7 +148,12 @@ local function get_completion_word(item) -- -- Typing `i` would remove the candidate because newText starts with `t`. local text = parse_snippet(item.insertText or item.textEdit.newText) - return #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label + local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label + if item.filterText and not match(word, prefix) then + return item.filterText + else + return word + end elseif item.insertText and item.insertText ~= '' then return parse_snippet(item.insertText) else @@ -276,7 +283,7 @@ function M._lsp_to_complete_items(result, prefix, client_id) 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) + local word = get_completion_word(item, prefix, match_item_by_value) local hl_group = '' if item.deprecated diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua @@ -216,6 +216,38 @@ describe('vim.lsp.completion: item conversion', function() }) end) + it('uses filterText as word if label/newText would not match', function() + local items = { + { + filterText = '<module', + insertTextFormat = 2, + kind = 10, + label = 'module', + sortText = 'module', + textEdit = { + newText = '<module>$1</module>$0', + range = { + start = { + character = 0, + line = 0, + }, + ['end'] = { + character = 0, + line = 0, + }, + }, + }, + }, + } + local expected = { + { + abbr = 'module', + word = '<module', + }, + } + assert_completion_matches('<mo', items, expected) + end) + it('fuzzy matches on label when filterText is missing', function() assert_completion_matches('fo', { { label = 'foo' },