diff options
author | Kristijan Husak <husakkristijan@gmail.com> | 2024-11-13 16:18:29 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-11-13 15:18:29 +0000 |
commit | 33d10db5b7a7cd03b22e3ba401cb3bc74640f522 (patch) | |
tree | 09175e05c0cc7c2f0ac9fb3d854f90553520564a | |
parent | 36990f324de2cfb96a6d2450e9c3ddfc3fba8afa (diff) | |
download | rneovim-33d10db5b7a7cd03b22e3ba401cb3bc74640f522.tar.gz rneovim-33d10db5b7a7cd03b22e3ba401cb3bc74640f522.tar.bz2 rneovim-33d10db5b7a7cd03b22e3ba401cb3bc74640f522.zip |
fix(lsp): filter completion candidates based on completeopt (#30945)
-rw-r--r-- | runtime/lua/vim/lsp/completion.lua | 26 | ||||
-rw-r--r-- | test/functional/plugin/lsp/completion_spec.lua | 266 |
2 files changed, 287 insertions, 5 deletions
diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 10086fa49e..92bc110a97 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -220,6 +220,20 @@ local function get_doc(item) return '' end +---@param value string +---@param prefix string +---@return boolean +local function match_item_by_value(value, prefix) + if vim.o.completeopt:find('fuzzy') ~= nil then + return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil + end + + if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then + return vim.startswith(value:lower(), prefix:lower()) + end + return vim.startswith(value, prefix) +end + --- Turns the result of a `textDocument/completion` request into vim-compatible --- |complete-items|. --- @@ -244,8 +258,16 @@ function M._lsp_to_complete_items(result, prefix, client_id) else ---@param item lsp.CompletionItem matches = function(item) - local text = item.filterText or item.label - return next(vim.fn.matchfuzzy({ text }, prefix)) ~= nil + if item.filterText then + return match_item_by_value(item.filterText, prefix) + end + + if item.textEdit then + -- server took care of filtering + return true + end + + return match_item_by_value(item.label, prefix) end end diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index ec2f88ec32..39b6ddc105 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -134,10 +134,14 @@ describe('vim.lsp.completion: item conversion', function() eq(expected, result) end) - it('filters on label if filterText is missing', function() + it('does not filter if there is a textEdit', function() + local range0 = { + start = { line = 0, character = 0 }, + ['end'] = { line = 0, character = 0 }, + } local completion_list = { - { label = 'foo' }, - { label = 'bar' }, + { label = 'foo', textEdit = { newText = 'foo', range = range0 } }, + { label = 'bar', textEdit = { newText = 'bar', range = range0 } }, } local result = complete('fo|', completion_list) local expected = { @@ -145,6 +149,10 @@ describe('vim.lsp.completion: item conversion', function() abbr = 'foo', word = 'foo', }, + { + abbr = 'bar', + word = 'bar', + }, } result = vim.tbl_map(function(x) return { @@ -152,9 +160,261 @@ describe('vim.lsp.completion: item conversion', function() word = x.word, } end, result.items) + local sorter = function(a, b) + return a.word > b.word + end + table.sort(expected, sorter) + table.sort(result, sorter) eq(expected, result) end) + ---@param prefix string + ---@param items lsp.CompletionItem[] + ---@param expected table[] + local assert_completion_matches = function(prefix, items, expected) + local result = complete(prefix .. '|', items) + result = vim.tbl_map(function(x) + return { + abbr = x.abbr, + word = x.word, + } + end, result.items) + local sorter = function(a, b) + return a.word > b.word + end + table.sort(expected, sorter) + table.sort(result, sorter) + eq(expected, result) + end + + describe('when completeopt has fuzzy matching enabled', function() + before_each(function() + exec_lua(function() + vim.opt.completeopt:append('fuzzy') + end) + end) + after_each(function() + exec_lua(function() + vim.opt.completeopt:remove('fuzzy') + end) + end) + + it('fuzzy matches on filterText', function() + assert_completion_matches('fo', { + { label = '?.foo', filterText = 'foo' }, + { label = 'faz other', filterText = 'faz other' }, + { label = 'bar', filterText = 'bar' }, + }, { + { + abbr = 'faz other', + word = 'faz other', + }, + { + abbr = '?.foo', + word = '?.foo', + }, + }) + end) + + it('fuzzy matches on label when filterText is missing', function() + assert_completion_matches('fo', { + { label = 'foo' }, + { label = 'faz other' }, + { label = 'bar' }, + }, { + { + abbr = 'faz other', + word = 'faz other', + }, + { + abbr = 'foo', + word = 'foo', + }, + }) + end) + end) + + describe('when smartcase is enabled', function() + before_each(function() + exec_lua(function() + vim.opt.smartcase = true + end) + end) + after_each(function() + exec_lua(function() + vim.opt.smartcase = false + end) + end) + + it('matches filterText case sensitively', function() + assert_completion_matches('Fo', { + { label = 'foo', filterText = 'foo' }, + { label = '?.Foo', filterText = 'Foo' }, + { label = 'Faz other', filterText = 'Faz other' }, + { label = 'faz other', filterText = 'faz other' }, + { label = 'bar', filterText = 'bar' }, + }, { + { + abbr = '?.Foo', + word = '?.Foo', + }, + }) + end) + + it('matches label case sensitively when filterText is missing', function() + assert_completion_matches('Fo', { + { label = 'foo' }, + { label = 'Foo' }, + { label = 'Faz other' }, + { label = 'faz other' }, + { label = 'bar' }, + }, { + { + abbr = 'Foo', + word = 'Foo', + }, + }) + end) + + describe('when ignorecase is enabled', function() + before_each(function() + exec_lua(function() + vim.opt.ignorecase = true + end) + end) + after_each(function() + exec_lua(function() + vim.opt.ignorecase = false + end) + end) + + it('matches filterText case insensitively if prefix is lowercase', function() + assert_completion_matches('fo', { + { label = '?.foo', filterText = 'foo' }, + { label = '?.Foo', filterText = 'Foo' }, + { label = 'Faz other', filterText = 'Faz other' }, + { label = 'faz other', filterText = 'faz other' }, + { label = 'bar', filterText = 'bar' }, + }, { + { + abbr = '?.Foo', + word = '?.Foo', + }, + { + abbr = '?.foo', + word = '?.foo', + }, + }) + end) + + it( + 'matches label case insensitively if prefix is lowercase and filterText is missing', + function() + assert_completion_matches('fo', { + { label = 'foo' }, + { label = 'Foo' }, + { label = 'Faz other' }, + { label = 'faz other' }, + { label = 'bar' }, + }, { + { + abbr = 'Foo', + word = 'Foo', + }, + { + abbr = 'foo', + word = 'foo', + }, + }) + end + ) + + it('matches filterText case sensitively if prefix has uppercase letters', function() + assert_completion_matches('Fo', { + { label = 'foo', filterText = 'foo' }, + { label = '?.Foo', filterText = 'Foo' }, + { label = 'Faz other', filterText = 'Faz other' }, + { label = 'faz other', filterText = 'faz other' }, + { label = 'bar', filterText = 'bar' }, + }, { + { + abbr = '?.Foo', + word = '?.Foo', + }, + }) + end) + + it( + 'matches label case sensitively if prefix has uppercase letters and filterText is missing', + function() + assert_completion_matches('Fo', { + { label = 'foo' }, + { label = 'Foo' }, + { label = 'Faz other' }, + { label = 'faz other' }, + { label = 'bar' }, + }, { + { + abbr = 'Foo', + word = 'Foo', + }, + }) + end + ) + end) + end) + + describe('when ignorecase is enabled', function() + before_each(function() + exec_lua(function() + vim.opt.ignorecase = true + end) + end) + after_each(function() + exec_lua(function() + vim.opt.ignorecase = false + end) + end) + + it('matches filterText case insensitively', function() + assert_completion_matches('Fo', { + { label = '?.foo', filterText = 'foo' }, + { label = '?.Foo', filterText = 'Foo' }, + { label = 'Faz other', filterText = 'Faz other' }, + { label = 'faz other', filterText = 'faz other' }, + { label = 'bar', filterText = 'bar' }, + }, { + { + abbr = '?.Foo', + word = '?.Foo', + }, + { + abbr = '?.foo', + word = '?.foo', + }, + }) + end) + + it('matches label case insensitively when filterText is missing', function() + assert_completion_matches('Fo', { + { label = 'foo' }, + { label = 'Foo' }, + { label = 'Faz other' }, + { label = 'faz other' }, + { label = 'bar' }, + }, { + { + abbr = 'Foo', + word = 'Foo', + }, + { + abbr = 'foo', + word = 'foo', + }, + }) + end) + end) + it('works on non word prefix', function() local completion_list = { { label = ' foo', insertText = '->foo' }, |