aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMathias Fussenegger <f.mathias@zignar.net>2024-05-28 23:20:25 +0200
committerMathias Fußenegger <mfussenegger@users.noreply.github.com>2024-05-30 09:24:24 +0200
commitb2bad0ac91ddb9b33c3547b6fd4f7278794818d9 (patch)
tree0dcf74de9a8ebbd54ceec230b11698fd9c4977e5
parent0df2c6b5d09fab392dd1a14e4b2e6a3b03203aaa (diff)
downloadrneovim-b2bad0ac91ddb9b33c3547b6fd4f7278794818d9.tar.gz
rneovim-b2bad0ac91ddb9b33c3547b6fd4f7278794818d9.tar.bz2
rneovim-b2bad0ac91ddb9b33c3547b6fd4f7278794818d9.zip
feat(lsp): support postfix snippets in completion
-rw-r--r--runtime/lua/vim/lsp/completion.lua35
-rw-r--r--test/functional/plugin/lsp/completion_spec.lua108
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)