fix(lsp): handle absence of a trailing newline #25194

Fixes #24339

rust-analyzer sends "Invalid offset" error in such cases. Some other
servers handle it specially.

LSP spec mentions that "A range is comparable to a selection in an
editor". Most editors don't handle trailing newlines the same way
Neovim/Vim does, it's clearly visible if it's present or not. With that
in mind it's understandable why sending end position as simply the start
of the line after the last one is considered invalid in such cases.
This commit is contained in:
Sergey Slipchenko 2023-09-21 14:06:40 +04:00 committed by GitHub
parent 8bd6f7c20b
commit 345bd91db2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 127 additions and 16 deletions

View File

@ -2230,6 +2230,35 @@ function M.lookup_section(settings, section)
return settings return settings
end end
--- Converts line range (0-based, end-inclusive) to lsp range,
--- handles absence of a trailing newline
---
---@param bufnr integer
---@param start_line integer
---@param end_line integer
---@param offset_encoding lsp.PositionEncodingKind
---@return lsp.Range
local function make_line_range_params(bufnr, start_line, end_line, offset_encoding)
local last_line = api.nvim_buf_line_count(bufnr) - 1
---@type lsp.Position
local end_pos
if end_line == last_line and not vim.api.nvim_get_option_value('endofline', { buf = bufnr }) then
end_pos = {
line = end_line,
character = M.character_offset(bufnr, end_line, #get_line(bufnr, end_line), offset_encoding),
}
else
end_pos = { line = end_line + 1, character = 0 }
end
return {
start = { line = start_line, character = 0 },
['end'] = end_pos,
}
end
---@private ---@private
--- Request updated LSP information for a buffer. --- Request updated LSP information for a buffer.
--- ---
@ -2253,6 +2282,8 @@ function M._refresh(method, opts)
return return
end end
local textDocument = M.make_text_document_params(bufnr)
local only_visible = opts.only_visible or false local only_visible = opts.only_visible or false
if only_visible then if only_visible then
@ -2260,28 +2291,25 @@ function M._refresh(method, opts)
if api.nvim_win_get_buf(window) == bufnr then if api.nvim_win_get_buf(window) == bufnr then
local first = vim.fn.line('w0', window) local first = vim.fn.line('w0', window)
local last = vim.fn.line('w$', window) local last = vim.fn.line('w$', window)
local params = {
textDocument = M.make_text_document_params(bufnr),
range = {
start = { line = first - 1, character = 0 },
['end'] = { line = last, character = 0 },
},
}
for _, client in ipairs(clients) do for _, client in ipairs(clients) do
client.request(method, params, nil, bufnr) client.request(method, {
textDocument = textDocument,
range = make_line_range_params(bufnr, first - 1, last - 1, client.offset_encoding),
}, nil, bufnr)
end end
end end
end end
else else
local params = {
textDocument = M.make_text_document_params(bufnr),
range = {
start = { line = 0, character = 0 },
['end'] = { line = api.nvim_buf_line_count(bufnr), character = 0 },
},
}
for _, client in ipairs(clients) do for _, client in ipairs(clients) do
client.request(method, params, nil, bufnr) client.request(method, {
textDocument = textDocument,
range = make_line_range_params(
bufnr,
0,
api.nvim_buf_line_count(bufnr) - 1,
client.offset_encoding
),
}, nil, bufnr)
end end
end end
end end

View File

@ -949,6 +949,28 @@ function tests.set_defaults_all_capabilities()
} }
end end
function tests.inlay_hint()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
inlayHintProvider = true;
}
}
end;
body = function()
notify('start')
expect_request('textDocument/inlayHint', function()
return nil, {}
end)
expect_notification("finish")
notify('finish')
end;
}
end
-- Tests will be indexed by test_name -- Tests will be indexed by test_name
local test_name = arg[1] local test_name = arg[1]
local timeout = arg[2] local timeout = arg[2]

View File

@ -1244,6 +1244,67 @@ describe('LSP', function()
} }
end) end)
it('should send correct range for inlay hints with noeol', function()
local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}};
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {
method="textDocument/inlayHint",
params = {
textDocument = {
uri = 'file://',
},
range = {
start = { line = 0, character = 0 },
['end'] = { line = 1, character = 3 },
}
},
bufnr=2,
client_id=1,
}};
{NIL, {}, {method="start", client_id=1}};
}
local client
test_rpc_server {
test_name = "inlay_hint";
on_setup = function()
exec_lua [[
BUFFER = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
"testing";
"123";
})
vim.bo[BUFFER].eol = false
]]
end;
on_init = function(_client)
client = _client
eq(true, client.supports_method('textDocument/inlayHint'))
exec_lua [[
assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
]]
end;
on_exit = function(code, signal)
eq(0, code, "exit code")
eq(0, signal, "exit signal")
end;
on_handler = function(err, result, ctx)
if ctx.method == 'start' then
exec_lua [[
vim.lsp.inlay_hint(BUFFER, true)
]]
end
if ctx.method == 'textDocument/inlayHint' then
client.notify('finish')
end
eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'finish' then
client.stop()
end
end;
}
end)
it('should check the body and didChange incremental', function() it('should check the body and didChange incremental', function()
local expected_handlers = { local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}}; {NIL, {}, {method="shutdown", client_id=1}};