aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKristijan Husak <husakkristijan@gmail.com>2024-11-13 16:18:29 +0100
committerGitHub <noreply@github.com>2024-11-13 15:18:29 +0000
commit33d10db5b7a7cd03b22e3ba401cb3bc74640f522 (patch)
tree09175e05c0cc7c2f0ac9fb3d854f90553520564a
parent36990f324de2cfb96a6d2450e9c3ddfc3fba8afa (diff)
downloadrneovim-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.lua26
-rw-r--r--test/functional/plugin/lsp/completion_spec.lua266
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' },