diff options
-rw-r--r-- | runtime/lua/vim/lsp/completion.lua | 35 | ||||
-rw-r--r-- | test/functional/plugin/lsp/completion_spec.lua | 108 |
2 files changed, 98 insertions, 45 deletions
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 diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index 078abdf653..d7755dd0c4 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -78,32 +78,6 @@ describe('vim.lsp.completion: item conversion', function() textEdit = { newText = 'foobar', range = range0 }, }, { label = 'foocar', sortText = 'f', textEdit = { newText = 'foobar', range = range0 } }, - -- real-world snippet text - { - label = 'foocar', - sortText = 'g', - insertText = 'foodar', - insertTextFormat = 2, - textEdit = { - newText = 'foobar(${1:place holder}, ${2:more ...holder{\\}})', - range = range0, - }, - }, - { - label = 'foocar', - sortText = 'h', - insertText = 'foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', - insertTextFormat = 2, - }, - -- nested snippet tokens - { - label = 'foocar', - sortText = 'i', - insertText = 'foodar(${1:${2|typ1,typ2|}}) {$0\\}', - insertTextFormat = 2, - }, - -- braced tabstop - { label = 'foocar', sortText = 'j', insertText = 'foodar()${0}', insertTextFormat = 2 }, -- plain text { label = 'foocar', @@ -139,23 +113,87 @@ describe('vim.lsp.completion: item conversion', function() }, { abbr = 'foocar', - word = 'foobar(place holder, more ...holder{})', + word = 'foodar(${1:var1})', -- marked as PlainText, text is used as is + }, + } + local result = complete('|', completion_list) + result = vim.tbl_map(function(x) + return { + abbr = x.abbr, + word = x.word, + } + end, result.items) + eq(expected, result) + end) + + it('prefers wordlike components for snippets', function() + -- There are two goals here: + -- + -- 1. The `word` should match what the user started typing, so that vim.fn.complete() doesn't + -- filter it away, preventing snippet expansion + -- + -- For example, if they type `items@ins`, luals returns `table.insert(items, $0)` as + -- textEdit.newText and `insert` as label. + -- There would be no prefix match if textEdit.newText is used as `word` + -- + -- 2. If users do not expand a snippet, but continue typing, they should see a somewhat reasonable + -- `word` getting inserted. + -- + -- For example in: + -- + -- insertText: "testSuites ${1:Env}" + -- label: "testSuites" + -- + -- "testSuites" should have priority as `word`, as long as the full snippet gets expanded on accept (<c-y>) + local range0 = { + start = { line = 0, character = 0 }, + ['end'] = { line = 0, character = 0 }, + } + local completion_list = { + -- luals postfix snippet (typed text: items@ins|) + { + label = 'insert', + insertTextFormat = 2, + textEdit = { + newText = 'table.insert(items, $0)', + range = range0, + }, }, + + -- eclipse.jdt.ls `new` snippet { - abbr = 'foocar', - word = 'foodar(var1 typ1, var2 *typ2) {}', + label = 'new', + insertTextFormat = 2, + textEdit = { + newText = '${1:Object} ${2:foo} = new ${1}(${3});\n${0}', + range = range0, + }, + textEditText = '${1:Object} ${2:foo} = new ${1}(${3});\n${0}', }, + + -- eclipse.jdt.ls `List.copyO` function call completion { - abbr = 'foocar', - word = 'foodar(typ1) {}', + label = 'copyOf(Collection<? extends E> coll) : List<E>', + insertTextFormat = 2, + insertText = 'copyOf', + textEdit = { + newText = 'copyOf(${1:coll})', + range = range0, + }, }, + } + local expected = { { - abbr = 'foocar', - word = 'foodar()', + abbr = 'copyOf(Collection<? extends E> coll) : List<E>', + word = 'copyOf', }, { - abbr = 'foocar', - word = 'foodar(${1:var1})', + abbr = 'insert', + word = 'insert', + }, + { + abbr = 'new', + word = 'new', }, } local result = complete('|', completion_list) |