From 6bb40f3dbffb4b9858d9b13486d1832db8f51755 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Wed, 31 Jul 2024 23:18:24 +0900 Subject: [PATCH] fix(lsp): prevent desync due to empty buffer (#29904) Problem: Some language servers (e.g., rust-analyzer, texlab) are desynced when the user deletes the entire contents of the buffer. This is due to the discrepancy between how nvim computes diff and how nvim treats empty buffer. * diff: If the buffer became empty, then the diff includes the last line's eol. * empty buffer: Even if the buffer is empty, nvim regards it as having a single empty line with eol. Solution: Add special case for diff computation when the buffer becomes empty so that it does not include the eol of the last line. --- runtime/lua/vim/lsp/sync.lua | 13 +++++- .../plugin/lsp/incremental_sync_spec.lua | 44 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua index 936579e003..bdfe8d51b8 100644 --- a/runtime/lua/vim/lsp/sync.lua +++ b/runtime/lua/vim/lsp/sync.lua @@ -212,7 +212,8 @@ end ---@param lastline integer ---@param new_lastline integer ---@param offset_encoding string ----@return vim.lsp.sync.Range, vim.lsp.sync.Range +---@return vim.lsp.sync.Range prev_end_range +---@return vim.lsp.sync.Range curr_end_range local function compute_end_range( prev_lines, curr_lines, @@ -222,6 +223,16 @@ local function compute_end_range( new_lastline, offset_encoding ) + -- A special case for the following `firstline == new_lastline` case where lines are deleted. + -- Even if the buffer has become empty, nvim behaves as if it has an empty line with eol. + if #curr_lines == 1 and curr_lines[1] == '' then + local prev_line = prev_lines[lastline - 1] + return { + line_idx = lastline - 1, + byte_idx = #prev_line + 1, + char_idx = compute_line_length(prev_line, offset_encoding) + 1, + }, { line_idx = 1, byte_idx = 1, char_idx = 1 } + end -- If firstline == new_lastline, the first change occurred on a line that was deleted. -- In this case, the last_byte... if firstline == new_lastline then diff --git a/test/functional/plugin/lsp/incremental_sync_spec.lua b/test/functional/plugin/lsp/incremental_sync_spec.lua index 238b90b57d..1e463d5117 100644 --- a/test/functional/plugin/lsp/incremental_sync_spec.lua +++ b/test/functional/plugin/lsp/incremental_sync_spec.lua @@ -170,7 +170,7 @@ describe('incremental synchronization', function() } test_edit({ 'a' }, { 'rb' }, expected_text_changes, 'utf-16', '\n') end) - it('deleting a line', function() + it('deleting the first line', function() local expected_text_changes = { { range = { @@ -183,11 +183,49 @@ describe('incremental synchronization', function() line = 1, }, }, - rangeLength = 12, + rangeLength = 6, text = '', }, } - test_edit({ 'hello world' }, { 'dd' }, expected_text_changes, 'utf-16', '\n') + test_edit({ 'hello', 'world' }, { 'ggdd' }, expected_text_changes, 'utf-16', '\n') + end) + it('deleting the last line', function() + local expected_text_changes = { + { + range = { + ['start'] = { + character = 0, + line = 1, + }, + ['end'] = { + character = 0, + line = 2, + }, + }, + rangeLength = 6, + text = '', + }, + } + test_edit({ 'hello', 'world' }, { '2ggdd' }, expected_text_changes, 'utf-16', '\n') + end) + it('deleting all lines', function() + local expected_text_changes = { + { + range = { + ['start'] = { + character = 0, + line = 0, + }, + ['end'] = { + character = 5, + line = 1, + }, + }, + rangeLength = 11, + text = '', + }, + } + test_edit({ 'hello', 'world' }, { 'ggdG' }, expected_text_changes, 'utf-16', '\n') end) it('deleting an empty line', function() local expected_text_changes = {