Files
neovim/test/functional/plugin/lsp/inline_completion_spec.lua
Luis Calle f3c2eb49ba feat: extend vim.Pos, vim.Range #36397
Problem:
Using nested `vim.Pos` objects to represent each `vim.Range` object
requires 3 tables for each `vim.Range`, which may be undesirable in
performance critical code. Using key-value tables performs worse than
using array-like tables (lists).

Solution:
Use array-like indices for the internal fields of both `vim.Pos` and
`vim.Range` objects. Use a metatable to allow users to access them like
if they were key-value tables.

---

Problem:
The `vim.Pos` conversion interface for `extmark` indexing does not take
into account the difference in how a position on top of a newline is
represented in `vim.Pos` and `extmark`.
- `vim.Pos`: for a newline at the end of row `n`, `row` takes the value
  `n + 1` and `col` takes the value `0`.
- `extmark`: for a newline at the end of for `n`, `row` takes the value
  `n` and `col` takes the value `#row_text`.

Solution:
Handle this in the `extmark` interface.

---

Problem:
Not all `to_xxx` interfaces have wrapping objects like `to_lsp`.

Solution:
Return unwrapped values in `to_xxx` interfaces where it makes sense.
Accept unwrapped values in "from" interfaces where it makes sense.

---

Problem:
`start` and `end` positions have different semantics, so they can't be
compared. `vim.Range` relies on comparing the `end` and `start` of two
ranges to decide which one is greater, which doesn't work as expected
because this of the different semantics.

For example, for the ranges:

    local a = {
      start = { row = 0, col = 22, },
      end_ = { row = 0, col = 24, },
    }
    local b = {
      start = { row = 0, col = 17, },
      end_ = { row = 0, col = 22, },
    }

in this code:

    local foo, bar = "foo",  "bar"
    --               |---||-|
    --                 b  a

The range `b` is smaller than the range `a`, but the current
implementation compares `b._end` (`col = 22`) and `a.start` (`col = 22`)
and concludes that, since `b.col` is not smaller than `a.col`, `b`
should be greater than `a`.

Solution:
- Use a `to_inclusive_pos` to normalize end positions inside of
  `vim.Range` whenever a comparison between a start and an end position
  is necessary.
2026-03-29 11:22:40 -04:00

332 lines
11 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 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)
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)