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.
This commit is contained in:
Jaehwang Jung 2024-07-31 23:18:24 +09:00 committed by GitHub
parent 4e90bc3023
commit 6bb40f3dbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 53 additions and 4 deletions

View File

@ -212,7 +212,8 @@ end
---@param lastline integer ---@param lastline integer
---@param new_lastline integer ---@param new_lastline integer
---@param offset_encoding string ---@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( local function compute_end_range(
prev_lines, prev_lines,
curr_lines, curr_lines,
@ -222,6 +223,16 @@ local function compute_end_range(
new_lastline, new_lastline,
offset_encoding 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. -- If firstline == new_lastline, the first change occurred on a line that was deleted.
-- In this case, the last_byte... -- In this case, the last_byte...
if firstline == new_lastline then if firstline == new_lastline then

View File

@ -170,7 +170,7 @@ describe('incremental synchronization', function()
} }
test_edit({ 'a' }, { 'rb' }, expected_text_changes, 'utf-16', '\n') test_edit({ 'a' }, { 'rb' }, expected_text_changes, 'utf-16', '\n')
end) end)
it('deleting a line', function() it('deleting the first line', function()
local expected_text_changes = { local expected_text_changes = {
{ {
range = { range = {
@ -183,11 +183,49 @@ describe('incremental synchronization', function()
line = 1, line = 1,
}, },
}, },
rangeLength = 12, rangeLength = 6,
text = '', 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) end)
it('deleting an empty line', function() it('deleting an empty line', function()
local expected_text_changes = { local expected_text_changes = {