Skip to content

Commit

Permalink
fix(completion): make completion request directly where possible
Browse files Browse the repository at this point in the history
Details:
- Do not force 'completefunc_lsp' call to make a completion LSP request.
  Instead make it explicitly and allow its callback to later trigger
  necessary keys. Among other things, this reduces flickering in case of
  `isIncomplete` update (as emulating 'completefunc_lsp' keys forces
  completion popup to immediately hide only to slightly later to
  reappear again).
- Being able to still make an LSP request directly inside
  `completefunc_lsp` is crucial to make it work with explicit `<C-x>...`
  and being able to retrigger completion at `<BS>`.
  • Loading branch information
echasnovski committed Feb 18, 2025
1 parent d6678c2 commit a84b7e5
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 5 deletions.
18 changes: 16 additions & 2 deletions lua/mini/completion.lua
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ MiniCompletion.completefunc_lsp = function(findstart, base)
end

-- NOTE: having code for request inside this function enables its use
-- directly with `<C-x><...>`.
-- directly with `<C-x><...>` and as a reaction to `<BS>`.
if H.completion.lsp.status ~= 'received' then
-- NOTE: it is CRUCIAL to make LSP request on the first call to
-- 'complete-function' (as in Vim's help). This is due to the fact that
Expand Down Expand Up @@ -656,6 +656,11 @@ H.auto_signature = function()
end

H.on_completedonepre = function()
-- Do nothing if it is triggered inside `trigger_lsp()` as a result of
-- emulating 'completefunc'/'omnifunc' keys. This can happen if popup is
-- visible and pressing keys first hides it with 'CompleteDonePre' event.
if H.completion.lsp.status == 'received' then return end

-- Try to apply additional text edits
H.apply_additional_text_edits()

Expand Down Expand Up @@ -713,6 +718,15 @@ H.trigger_lsp = function()
-- Do not trigger if not needed and/or allowed
if vim.fn.mode() ~= 'i' or (H.pumvisible() and not H.completion.force) then return end

-- Overall idea: first make LSP request and re-trigger this same function
-- inside its callback to take the "received" route. This reduces flickering
-- in case popup is visible (like for `isIncomplete` and trigger characters)
-- as pressing 'completefunc'/'omnifunc' keys first hides completion menu.
-- There are still minor visual defects: typing new character reduces number
-- of matched items which can visually shrink popup while later increase it
-- again after LSP response is received. This is usually fine (especially
-- with not huge 'pumheight').
if H.completion.lsp.status ~= 'received' then return H.make_completion_request() end
local keys = H.keys[H.get_config().lsp_completion.source_func]
vim.api.nvim_feedkeys(keys, 'n', false)
end
Expand Down Expand Up @@ -845,7 +859,7 @@ H.make_completion_request = function()
H.completion.lsp.status = 'received'
H.completion.lsp.result = result

-- Trigger LSP completion to use "received" route
-- Trigger LSP completion to use completefunc/omnifunc route
H.trigger_lsp()
end)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--|---------|---------|
01|Jun
02|June Function
03|~
04|~
05|~
06|~
07|~
08|~
09|<ame] [+] 1,4 All
10|-- INSERT --

--|---------|---------|
01|00000000000000000000
02|11111111111111122222
03|22222222222222222222
04|22222222222222222222
05|22222222222222222222
06|22222222222222222222
07|22222222222222222222
08|22222222222222222222
09|33333333333333333333
10|44444444444455555555
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--|---------|---------|
01|Ju
02|June Function
03|July Function
04|~
05|~
06|~
07|~
08|~
09|<ame] [+] 1,3 All
10|-- INSERT --

--|---------|---------|
01|00000000000000000000
02|11111111111111122222
03|11111111111111122222
04|22222222222222222222
05|22222222222222222222
06|22222222222222222222
07|22222222222222222222
08|22222222222222222222
09|33333333333333333333
10|44444444444455555555
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--|---------|---------|
01|J
02|January Text
03|June Function
04|July Function
05|~
06|~
07|~
08|~
09|<ame] [+] 1,2 All
10|-- INSERT --

--|---------|---------|
01|00000000000000000000
02|11111111111111111222
03|11111111111111111222
04|11111111111111111222
05|22222222222222222222
06|22222222222222222222
07|22222222222222222222
08|22222222222222222222
09|33333333333333333333
10|44444444444455555555
79 changes: 76 additions & 3 deletions tests/test_completion.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ local mock_miniicons = function()
]])
end

local mock_completefunc_lsp_tracking = function()
child.lua([[
local completefunc_lsp_orig = MiniCompletion.completefunc_lsp
MiniCompletion.completefunc_lsp = function(...)
_G.n_completfunc_lsp = (_G.n_completfunc_lsp or 0) + 1
return completefunc_lsp_orig(...)
end
]])
end

-- NOTE: this can't show "what filtered text is actually shown in window".
-- Seems to be because information for `complete_info()`
--- is updated in the very last minute (probably, by UI). This means that the
Expand Down Expand Up @@ -375,8 +385,9 @@ T['Autocompletion']['forces new LSP completion at LSP trigger'] = new_set(
{ parametrize = { { 'completefunc' }, { 'omnifunc' } } },
{
test = function(source_func)
child.set_size(16, 20)
reload_module({ lsp_completion = { source_func = source_func } })
mock_completefunc_lsp_tracking()
child.set_size(16, 20)
child.api.nvim_set_current_buf(child.api.nvim_create_buf(true, false))

--stylua: ignore
Expand All @@ -386,13 +397,18 @@ T['Autocompletion']['forces new LSP completion at LSP trigger'] = new_set(
}
type_keys('i', '<C-Space>')
eq(get_completion(), all_months)
-- - Should only call two times, as per `:h complete-functions`, i.e. not
-- use it to actually make request (which would have made it 3 times).
eq(child.lua_get('_G.n_completfunc_lsp'), 2)

type_keys('May.')
eq(child.lua_get('_G.n_completfunc_lsp'), 2)
eq(child.fn.pumvisible(), 0)
sleep(default_completion_delay - small_time)
eq(child.fn.pumvisible(), 0)
sleep(small_time + small_time)
eq(get_completion(), all_months)
eq(child.lua_get('_G.n_completfunc_lsp'), 4)
child.expect_screenshot()

-- Should show only LSP without fallback, i.e. typing LSP trigger should
Expand All @@ -405,34 +421,74 @@ T['Autocompletion']['forces new LSP completion at LSP trigger'] = new_set(
}
)

T['Autocompletion']['works with `<BS>`'] = function()
mock_completefunc_lsp_tracking()
child.set_size(10, 20)
child.api.nvim_set_current_buf(child.api.nvim_create_buf(true, false))

type_keys('i', 'J', 'u', '<C-Space>')
-- - Should only call two times, as per `:h complete-functions`: first to
-- find the start column, second to return completion suggestions.
eq(child.lua_get('_G.n_completfunc_lsp'), 2)

type_keys('n')
child.expect_screenshot()
eq(child.lua_get('_G.n_completfunc_lsp'), 2)

-- Should keep completion menu and adjust items without extra 'completefunc'
-- calls (as it is still at or after initial start column)
type_keys('<BS>')
child.expect_screenshot()
eq(child.lua_get('_G.n_completfunc_lsp'), 2)

-- Should reevaluate completion list as it is past initial start column
type_keys('<BS>')
child.expect_screenshot()
-- - Should call three times: first to make a request, second and third to
-- act as a regular 'completefunc'/'omnifunc'
eq(child.lua_get('_G.n_completfunc_lsp'), 5)
end

T['Autocompletion']['forces new LSP completion in case of `isIncomplete`'] = function()
mock_completefunc_lsp_tracking()
child.set_size(10, 20)
child.api.nvim_set_current_buf(child.api.nvim_create_buf(true, false))

-- Mock incomplete completion list which contains only months 1-6
child.lua('_G.mock_isincomplete = true')
type_keys('i', 'J', '<C-Space>')
-- Should not contain `July` as it is not in the response
-- - Should not contain `July` as it is not in the response
child.expect_screenshot()
eq(child.lua_get('_G.n_textdocument_completion'), 1)
-- - Should only call two times, as per `:h complete-functions`: first to
-- find the start column, second to return completion suggestions.
eq(child.lua_get('_G.n_completfunc_lsp'), 2)

-- Should force new request which this time will be complete
child.lua('_G.mock_isincomplete = false')
type_keys('u')
child.expect_screenshot()
eq(child.lua_get('_G.n_textdocument_completion'), 2)
-- - NOTE: not using completefunc to make an LSP request is a key to reduce
-- flickering in this use case (as executing `<C-x>...` forces popup hide).
eq(child.lua_get('_G.n_completfunc_lsp'), 4)

-- Should *not* force new requests for complete responses
-- Shouldn't force new requests or call 'completefunc' for complete responses
type_keys('n')
eq(child.lua_get('_G.n_textdocument_completion'), 2)
eq(child.lua_get('_G.n_completfunc_lsp'), 4)
type_keys('<BS>')
eq(child.lua_get('_G.n_textdocument_completion'), 2)
eq(child.lua_get('_G.n_completfunc_lsp'), 4)

-- Should force new request if deleting past the start of previous request.
-- This time response will be complete.
type_keys('<BS>')
child.expect_screenshot()
eq(child.lua_get('_G.n_textdocument_completion'), 3)
-- - Should call three times: first to make a request, second and third to
-- act as a regular 'completefunc'/'omnifunc'
eq(child.lua_get('_G.n_completfunc_lsp'), 7)
end

T['Autocompletion']['respects `config.delay.completion`'] = function()
Expand Down Expand Up @@ -565,6 +621,23 @@ T['Manual completion']['works with fallback action'] = function()
eq(get_completion(), { 'Jackpot' })
end

T['Manual completion']['works with explicit `<C-x>...`'] = new_set(
{ parametrize = { { 'completefunc' }, { 'omnifunc' } } },
{
test = function(source_func)
reload_module({ lsp_completion = { source_func = source_func } })
mock_completefunc_lsp_tracking()
child.api.nvim_set_current_buf(child.api.nvim_create_buf(true, false))

local source_keys = '<C-x>' .. (source_func == 'completefunc' and '<C-u>' or '<C-o>')
type_keys('i', 'J', source_keys)
eq(get_completion(), { 'January', 'June', 'July' })
-- Should call three times: first to initiate request, second/third to
-- perform its actions (as per `:h complete-functions`)
eq(child.lua_get('_G.n_completfunc_lsp'), 3)
end,
}
)
T['Manual completion']['respects `config.mappings'] = function()
reload_module({ mappings = { force_twostep = '<C-z>', force_fallback = '<C-x>' } })
type_keys('i', 'J', '<C-z>')
Expand Down

0 comments on commit a84b7e5

Please sign in to comment.