From 2c5f37f6af962bc9ed65ca87b8272800badfb78f Mon Sep 17 00:00:00 2001 From: glepnir Date: Sat, 22 Aug 2026 19:40:17 +0800 Subject: [PATCH] fix(lsp): empty filterText/sortText/insertText not treated as unset #41421 Problem: Empty string is falsy, so the spec's label fallback applies, but there only checks for nil. An empty filterText drops the item, an empty sortText sorts it first, and an empty insertText leaves the word empty. Solution: Treat empty string as unset. --- runtime/lua/vim/lsp/completion.lua | 26 ++++++++++++------- .../functional/plugin/lsp/completion_spec.lua | 14 ++++++++-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 3b2724b170..f1a17e3c4a 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -160,6 +160,12 @@ local function has_completeopt(flag) return vim.list_contains(vim.opt.completeopt:get(), flag) end +--- @param s string? +--- @return string? +local function nonempty(s) + return s ~= '' and s or nil +end + --- @param input string Unparsed snippet --- @return string # Parsed snippet if successful, else returns its input local function parse_snippet(input) @@ -211,12 +217,10 @@ local function get_completion_word(item, prefix, match) -- label: insert -- -- Typing `i` would remove the candidate because newText starts with `t`. - local text = parse_snippet(item.insertText or item.textEdit.newText) + local text = parse_snippet(nonempty(item.insertText) or item.textEdit.newText) + local filter_text = nonempty(item.filterText) local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') - or ( - item.filterText and vim.fn.match(item.label, '^\\k') == -1 and item.filterText - or item.label - ) + or (filter_text and vim.fn.match(item.label, '^\\k') == -1 and filter_text or item.label) return fallback_filtertext(item, word, prefix, match) else return item.label @@ -266,7 +270,10 @@ local function apply_defaults(item, defaults, apply_kind) if defaults.editRange then local textEdit = item.textEdit or {} item.textEdit = textEdit - textEdit.newText = textEdit.newText or item.textEditText or item.insertText or item.label + textEdit.newText = textEdit.newText + or item.textEditText + or nonempty(item.insertText) + or item.label if defaults.editRange.start then textEdit.range = textEdit.range or defaults.editRange elseif defaults.editRange.insert then @@ -481,8 +488,9 @@ function M._lsp_to_complete_items( else ---@param item lsp.CompletionItem matches = function(item) - if item.filterText then - return match_item_by_value(item.filterText, prefix) + local filter_text = nonempty(item.filterText) + if filter_text then + return match_item_by_value(filter_text, prefix) end if item.textEdit and not item.textEdit.newText then @@ -593,7 +601,7 @@ function M._lsp_to_complete_items( 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) + return (nonempty(itema.sortText) or itema.label) < (nonempty(itemb.sortText) or itemb.label) end local use_fuzzy_sort = has_completeopt('fuzzy') diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index 2a7fa0df81..775e3e7dd8 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -135,6 +135,14 @@ describe('vim.lsp.completion: item conversion', function() range = range0, }, }, + -- "" is unset + { + label = 'zoocar_long', + sortText = '', + insertText = '', + insertTextFormat = 2, + textEdit = { newText = 'foobar($1)', range = range0 }, + }, } local expected = { { abbr = 'foobar', word = 'foobar' }, @@ -145,6 +153,7 @@ describe('vim.lsp.completion: item conversion', function() { abbr = 'foocar', word = 'foobar' }, { abbr = 'foocar', word = 'foodar(${1:var1})' }, -- marked as PlainText, text is used as is { abbr = '•INT16_C(c)', word = 'INT16_C' }, + { abbr = 'zoocar_long', word = 'foobar' }, } local result = complete('|', completion_list) eq(expected, extract_word_abbr(result.items)) @@ -299,9 +308,9 @@ describe('vim.lsp.completion: item conversion', function() }) end) - it('fuzzy matches on label when filterText is missing', function() + it('fuzzy matches on label when filterText is missing or empty', function() assert_completion_matches('fo', { - { label = 'foo' }, + { label = 'foo', filterText = '' }, { label = 'faz other' }, { label = 'bar' }, }, { @@ -755,6 +764,7 @@ describe('vim.lsp.completion: item conversion', function() { label = 'hello', data = 'item-property-has-priority', + insertText = '', }, }, }