fix(lsp): send didOpen if name changes on write (#19583)

`:saveas newName` changes the name of an existing buffer.
Due to the buffer re-use it skips the lsp attach phase and immediately
sends a `didSave` notification to the server.
Servers get confused about this, because they expect a `didOpen`
notification first.

Closes https://github.com/neovim/neovim/issues/18688
This commit is contained in:
Mathias Fußenegger
2022-08-01 22:32:53 +02:00
committed by GitHub
parent 711ef4eac9
commit e99de3f12f
3 changed files with 91 additions and 5 deletions

View File

@@ -371,7 +371,9 @@ do
state_by_client[client.id] = state state_by_client[client.id] = state
end end
if not state.buffers[bufnr] then if not state.buffers[bufnr] then
local buf_state = {} local buf_state = {
name = api.nvim_buf_get_name(bufnr),
}
state.buffers[bufnr] = buf_state state.buffers[bufnr] = buf_state
if use_incremental_sync then if use_incremental_sync then
buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true) buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true)
@@ -381,6 +383,15 @@ do
end end
end end
---@private
function changetracking._get_and_set_name(client, bufnr, name)
local state = state_by_client[client.id] or {}
local buf_state = (state.buffers or {})[bufnr]
local old_name = buf_state.name
buf_state.name = name
return old_name
end
---@private ---@private
function changetracking.reset_buf(client, bufnr) function changetracking.reset_buf(client, bufnr)
changetracking.flush(client, bufnr) changetracking.flush(client, bufnr)
@@ -1405,6 +1416,19 @@ local function text_document_did_save_handler(bufnr)
local uri = vim.uri_from_bufnr(bufnr) local uri = vim.uri_from_bufnr(bufnr)
local text = once(buf_get_full_text) local text = once(buf_get_full_text)
for_each_buffer_client(bufnr, function(client) for_each_buffer_client(bufnr, function(client)
local name = api.nvim_buf_get_name(bufnr)
local old_name = changetracking._get_and_set_name(client, bufnr, name)
if old_name and name ~= old_name then
client.notify('textDocument/didOpen', {
textDocument = {
version = 0,
uri = uri,
languageId = client.config.get_language_id(bufnr, vim.bo[bufnr].filetype),
text = buf_get_full_text(bufnr),
},
})
util.buf_versions[bufnr] = 0
end
local save_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'save') local save_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'save')
if save_capability then if save_capability then
local included_text local included_text

View File

@@ -67,11 +67,13 @@ local function expect_notification(method, params, ...)
local message = read_message() local message = read_message()
assert_eq(method, message.method, assert_eq(method, message.method,
..., "expect_notification", "method") ..., "expect_notification", "method")
if params then
assert_eq(params, message.params, assert_eq(params, message.params,
..., "expect_notification", method, "params") ..., "expect_notification", method, "params")
assert_eq({jsonrpc = "2.0"; method=method, params=params}, message, assert_eq({jsonrpc = "2.0"; method=method, params=params}, message,
..., "expect_notification", "message") ..., "expect_notification", "message")
end end
end
local function expect_request(method, handler, ...) local function expect_request(method, handler, ...)
local req = read_message() local req = read_message()
@@ -257,6 +259,26 @@ function tests.basic_check_capabilities()
} }
end end
function tests.text_document_save_did_open()
skeleton {
on_init = function()
return {
capabilities = {
textDocumentSync = {
save = true
}
}
}
end;
body = function()
notify('start')
expect_notification('textDocument/didOpen')
expect_notification('textDocument/didSave')
notify('shutdown')
end;
}
end
function tests.text_document_sync_save_bool() function tests.text_document_sync_save_bool()
skeleton { skeleton {
on_init = function() on_init = function()

View File

@@ -535,6 +535,46 @@ describe('LSP', function()
} }
end) end)
it('saveas sends didOpen if filename changed', function()
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
}
local client
test_rpc_server({
test_name = 'text_document_save_did_open',
on_init = function(c)
client = c
end,
on_exit = function(code, signal)
eq(0, code, 'exit code')
eq(0, signal, 'exit signal')
end,
on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
if ctx.method == 'start' then
local tmpfile_old = helpers.tmpname()
local tmpfile_new = helpers.tmpname()
os.remove(tmpfile_new)
exec_lua(
[=[
local oldname, newname = ...
BUFFER = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_name(BUFFER, oldname)
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, true, {"help me"})
lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
vim.api.nvim_buf_call(BUFFER, function() vim.cmd('saveas ' .. newname) end)
]=],
tmpfile_old,
tmpfile_new
)
else
client.stop()
end
end,
})
end)
it('BufWritePost sends didSave including text if server capability is set', function() it('BufWritePost sends didSave including text if server capability is set', function()
local expected_handlers = { local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}}; {NIL, {}, {method="shutdown", client_id=1}};