commit 345bd91db28ecfc4deb308f4971253b534f82d49
parent 8bd6f7c20b403e8031a94f3a158a10c90b5c3efd
Author: Sergey Slipchenko <faergeek@gmail.com>
Date: Thu, 21 Sep 2023 14:06:40 +0400
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.
Diffstat:
3 files changed, 127 insertions(+), 16 deletions(-)
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
@@ -2230,6 +2230,35 @@ function M.lookup_section(settings, section)
return settings
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
--- Request updated LSP information for a buffer.
---
@@ -2253,6 +2282,8 @@ function M._refresh(method, opts)
return
end
+ local textDocument = M.make_text_document_params(bufnr)
+
local only_visible = opts.only_visible or false
if only_visible then
@@ -2260,28 +2291,25 @@ function M._refresh(method, opts)
if api.nvim_win_get_buf(window) == bufnr then
local first = vim.fn.line('w0', 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
- 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
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
- 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
diff --git a/test/functional/fixtures/fake-lsp-server.lua b/test/functional/fixtures/fake-lsp-server.lua
@@ -949,6 +949,28 @@ function tests.set_defaults_all_capabilities()
}
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
local test_name = arg[1]
local timeout = arg[2]
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
@@ -1244,6 +1244,67 @@ describe('LSP', function()
}
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()
local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}};