mirror of
https://github.com/neovim/neovim.git
synced 2026-09-02 04:13:48 +00:00
Problem:
`Completor:accept()` hands the item's range straight to
`nvim_buf_set_text()`. That range is resolved when the response arrives,
so text deleted before accepting (backspacing within the 200 ms
debounce) leaves the range end past what is left of the line, and the
accept aborts with `Invalid 'end_col': out of range`.
18d6436 fixed the same staleness in `Completor:show()`, which drops an
item once its range *start* falls outside the buffer. That guard does
not cover the range end, and `accept()` was never given one, so this is
a hole left by that fix rather than a regression of it.
Solution:
Clamp the end of the range to the end of the line before writing. The
start needs no clamp: `show()` dropped the item one event loop tick
earlier if it had gone out of range.
346 lines
12 KiB
Lua
346 lines
12 KiB
Lua
local t = require('test.testutil')
|
|
local n = require('test.functional.testnvim')()
|
|
local t_lsp = require('test.functional.plugin.lsp.testutil')
|
|
local Screen = require('test.functional.ui.screen')
|
|
|
|
local describe, it, before_each, after_each = t.describe, t.it, t.before_each, t.after_each
|
|
local dedent = t.dedent
|
|
local eq = t.eq
|
|
|
|
local api = n.api
|
|
local exec_lua = n.exec_lua
|
|
local insert = n.insert
|
|
local feed = n.feed
|
|
|
|
local clear_notrace = t_lsp.clear_notrace
|
|
local create_server_definition = t_lsp.create_server_definition
|
|
|
|
describe('vim.lsp.inline_completion', function()
|
|
local text = dedent([[
|
|
function fibonacci()
|
|
]])
|
|
|
|
local grid_without_candidates = dedent([[
|
|
function fibonacci() |
|
|
^ |
|
|
{1:~ }|*11
|
|
|
|
|
]])
|
|
|
|
local grid_with_candidates = dedent([[
|
|
function fibonacci({1:n) {} |
|
|
{1: if (n <= 0) return 0;} |
|
|
{1: if (n === 1) return 1;} |
|
|
|
|
|
{1: let a = 0, b = 1, c;} |
|
|
{1: for (let i = 2; i <= n; i++) {} |
|
|
{1: c = a + b;} |
|
|
{1: a = b;} |
|
|
{1: b = c;} |
|
|
{1: }} |
|
|
{1: return b;} |
|
|
{1:}} |
|
|
^ |
|
|
{3:-- INSERT --} |
|
|
]])
|
|
|
|
local grid_applied_candidates = dedent([[
|
|
function fibonacci(n) { |
|
|
if (n <= 0) return 0; |
|
|
if (n === 1) return 1; |
|
|
|
|
|
let a = 0, b = 1, c; |
|
|
for (let i = 2; i <= n; i++) { |
|
|
c = a + b; |
|
|
a = b; |
|
|
b = c; |
|
|
} |
|
|
return b; |
|
|
^} |
|
|
|*2
|
|
]])
|
|
|
|
--- @type test.functional.ui.screen
|
|
local screen
|
|
|
|
--- @type integer
|
|
local client_id
|
|
|
|
before_each(function()
|
|
clear_notrace()
|
|
exec_lua(create_server_definition)
|
|
|
|
screen = Screen.new()
|
|
screen:set_default_attr_ids({
|
|
[1] = { bold = true, foreground = Screen.colors.Blue1 },
|
|
[2] = { bold = true, foreground = Screen.colors.SeaGreen4 },
|
|
[3] = { bold = true },
|
|
})
|
|
|
|
client_id = exec_lua(function()
|
|
_G.server = _G._create_server({
|
|
capabilities = {
|
|
inlineCompletionProvider = true,
|
|
},
|
|
handlers = {
|
|
['textDocument/inlineCompletion'] = function(_, _, callback)
|
|
if _G.empty then
|
|
callback(nil, {
|
|
items = {
|
|
{
|
|
insertText = 'foobar',
|
|
range = {
|
|
start = {
|
|
line = 0,
|
|
character = 19,
|
|
},
|
|
['end'] = {
|
|
line = 0,
|
|
character = 19,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
return
|
|
end
|
|
|
|
callback(nil, {
|
|
items = {
|
|
{
|
|
command = {
|
|
command = 'dummy',
|
|
title = 'Completion Accepted',
|
|
},
|
|
insertText = 'function fibonacci(n) {\n if (n <= 0) return 0;\n if (n === 1) return 1;\n\n let a = 0, b = 1, c;\n for (let i = 2; i <= n; i++) {\n c = a + b;\n a = b;\n b = c;\n }\n return b;\n}',
|
|
range = {
|
|
['end'] = {
|
|
character = 20,
|
|
line = 0,
|
|
},
|
|
start = {
|
|
character = 0,
|
|
line = 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command = {
|
|
command = 'dummy',
|
|
title = 'Completion Accepted',
|
|
},
|
|
insertText = 'function fibonacci(n) {\n if (n <= 0) return 0;\n if (n === 1) return 1;\n\n let a = 0, b = 1, c;\n for (let i = 2; i <= n; i++) {\n c = a + b;\n a = b;\n b = c;\n }\n return c;\n}',
|
|
range = {
|
|
['end'] = {
|
|
character = 20,
|
|
line = 0,
|
|
},
|
|
start = {
|
|
character = 0,
|
|
line = 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command = {
|
|
command = 'dummy',
|
|
title = 'Completion Accepted',
|
|
},
|
|
insertText = 'function fibonacci(n) {\n if (n < 0) {\n throw new Error("Input must be a non-negative integer.");\n }\n if (n === 0) return 0;\n if (n === 1) return 1;\n\n let a = 0, b = 1, c;\n for (let i = 2; i <= n; i++) {\n c = a + b;\n a = b;\n b = c;\n }\n return b;\n}',
|
|
range = {
|
|
['end'] = {
|
|
character = 20,
|
|
line = 0,
|
|
},
|
|
start = {
|
|
character = 0,
|
|
line = 0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
end,
|
|
},
|
|
})
|
|
|
|
return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })
|
|
end)
|
|
|
|
exec_lua(function()
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
_G.called = false
|
|
client.commands.dummy = function()
|
|
_G.called = true
|
|
end
|
|
end)
|
|
|
|
insert(text)
|
|
feed('$')
|
|
exec_lua(function()
|
|
vim.lsp.inline_completion.enable()
|
|
end)
|
|
end)
|
|
|
|
after_each(function()
|
|
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
|
|
end)
|
|
|
|
describe('enable()', function()
|
|
it('requests or abort when entered/left insert mode', function()
|
|
screen:expect({ grid = grid_without_candidates })
|
|
feed('i')
|
|
screen:expect({ grid = grid_with_candidates })
|
|
feed('<Esc>')
|
|
screen:expect({ grid = grid_without_candidates })
|
|
end)
|
|
|
|
it('no request when leaving insert mode immediately after typing', function()
|
|
screen:expect({ grid = grid_without_candidates })
|
|
feed('ifoobar<Esc>')
|
|
screen:expect([[
|
|
function fibonacci() |
|
|
fooba^r |
|
|
{1:~ }|*11
|
|
|
|
|
]])
|
|
screen:expect_unchanged(false, 500)
|
|
end)
|
|
end)
|
|
|
|
describe('get()', function()
|
|
it('applies the current candidate', function()
|
|
feed('i')
|
|
screen:expect({ grid = grid_with_candidates })
|
|
exec_lua(function()
|
|
vim.lsp.inline_completion.get()
|
|
end)
|
|
n.poke_eventloop()
|
|
feed('<Esc>')
|
|
screen:expect({ grid = grid_applied_candidates })
|
|
end)
|
|
|
|
it('correctly displays with absent/empty range', function()
|
|
exec_lua(function()
|
|
_G.empty = true
|
|
end)
|
|
feed('I')
|
|
screen:expect([[
|
|
function fibonacci({1:foobar}) |
|
|
^ |
|
|
{1:~ }|*11
|
|
{3:-- INSERT --} |
|
|
]])
|
|
end)
|
|
|
|
it('accepts on_accept callback', function()
|
|
feed('i')
|
|
screen:expect({ grid = grid_with_candidates })
|
|
local result = exec_lua(function()
|
|
---@type vim.lsp.inline_completion.Item
|
|
local result
|
|
vim.lsp.inline_completion.get({
|
|
on_accept = function(item)
|
|
result = item
|
|
end,
|
|
})
|
|
vim.wait(1000, function()
|
|
return result ~= nil
|
|
end) -- Wait for async callback.
|
|
return result
|
|
end)
|
|
feed('<Esc>')
|
|
screen:expect({ grid = grid_without_candidates })
|
|
eq({
|
|
_index = 1,
|
|
client_id = 1,
|
|
command = {
|
|
command = 'dummy',
|
|
title = 'Completion Accepted',
|
|
},
|
|
insert_text = dedent([[
|
|
function fibonacci(n) {
|
|
if (n <= 0) return 0;
|
|
if (n === 1) return 1;
|
|
|
|
let a = 0, b = 1, c;
|
|
for (let i = 2; i <= n; i++) {
|
|
c = a + b;
|
|
a = b;
|
|
b = c;
|
|
}
|
|
return b;
|
|
}]]),
|
|
range = {
|
|
0,
|
|
0,
|
|
0,
|
|
20,
|
|
1,
|
|
},
|
|
}, result)
|
|
end)
|
|
|
|
it('accepts an item after the line shrank past its range', function()
|
|
feed('i')
|
|
screen:expect({ grid = grid_with_candidates })
|
|
exec_lua(function()
|
|
-- Shrink the line the item's range covers, as backspacing would.
|
|
vim.api.nvim_buf_set_text(0, 0, 18, 0, 20, {})
|
|
vim.lsp.inline_completion.get()
|
|
end)
|
|
n.poke_eventloop()
|
|
feed('<Esc>')
|
|
screen:expect({ grid = grid_applied_candidates })
|
|
end)
|
|
end)
|
|
|
|
describe('select()', function()
|
|
it('selects the next candidate', function()
|
|
feed('i')
|
|
screen:expect({ grid = grid_with_candidates })
|
|
|
|
exec_lua(function()
|
|
vim.lsp.inline_completion.select()
|
|
end)
|
|
|
|
screen:expect([[
|
|
function fibonacci({1:n) {} |
|
|
{1: if (n <= 0) return 0;} |
|
|
{1: if (n === 1) return 1;} |
|
|
|
|
|
{1: let a = 0, b = 1, c;} |
|
|
{1: for (let i = 2; i <= n; i++) {} |
|
|
{1: c = a + b;} |
|
|
{1: a = b;} |
|
|
{1: b = c;} |
|
|
{1: }} |
|
|
{1: return c;} |
|
|
{1:}}{2: (2/3)} |
|
|
^ |
|
|
{3:-- INSERT --} |
|
|
]])
|
|
exec_lua(function()
|
|
vim.lsp.inline_completion.get()
|
|
end)
|
|
n.poke_eventloop()
|
|
feed('<Esc>')
|
|
screen:expect([[
|
|
function fibonacci(n) { |
|
|
if (n <= 0) return 0; |
|
|
if (n === 1) return 1; |
|
|
|
|
|
let a = 0, b = 1, c; |
|
|
for (let i = 2; i <= n; i++) { |
|
|
c = a + b; |
|
|
a = b; |
|
|
b = c; |
|
|
} |
|
|
return c; |
|
|
^} |
|
|
|*2
|
|
]])
|
|
end)
|
|
end)
|
|
end)
|