fix(diagnostics): position diagnostics using extmarks #34014

Problem:
Diagnostic positions are not being updated after text changes, which
means `vim.diagnostic.open_float` and `vim.diagnostic.jump` will work
with outdated positions when text is changed until diagnostics are
updated again (if ever).

Solution:
Create extmarks in `vim.diagnostic.set` and use their positions for
`vim.diagnostic.open_float` and `next_diagnostic` (used by
`vim.diagnostic.jump`, `vim.diagnostic.get_next` and
`vim.diagnostic.get_prev`).
This commit is contained in:
Sergei Slipchenko
2025-07-25 18:56:50 +04:00
committed by GitHub
parent e4a100a1e1
commit 0a113013fb
2 changed files with 264 additions and 18 deletions

View File

@@ -1430,6 +1430,77 @@ describe('vim.diagnostic', function()
eq(true, exec_lua('return _G.jumped'))
end)
end)
describe('after inserting text before diagnostic position', function()
before_each(function()
exec_lua(function()
vim.api.nvim_set_current_buf(_G.diagnostic_bufnr)
vim.diagnostic.set(_G.diagnostic_ns, _G.diagnostic_bufnr, {
_G.make_error('Diagnostic #1', 1, 4, 1, 7),
_G.make_error('Diagnostic #2', 3, 0, 3, 3),
})
end)
api.nvim_buf_set_text(0, 3, 0, 3, 0, { 'new line', 'new ' })
end)
it('finds next diagnostic at a logical location', function()
eq(
{ 5, 4 },
exec_lua(function()
vim.api.nvim_win_set_cursor(0, { 2, 4 })
vim.diagnostic.jump({ count = 1 })
return vim.api.nvim_win_get_cursor(0)
end)
)
end)
it('finds previous diagnostic at a logical location', function()
eq(
{ 5, 4 },
exec_lua(function()
vim.api.nvim_win_set_cursor(0, { 6, 4 })
vim.diagnostic.jump({ count = -1 })
return vim.api.nvim_win_get_cursor(0)
end)
)
end)
end)
describe('if diagnostic is set after last character in line', function()
before_each(function()
exec_lua(function()
vim.api.nvim_set_current_buf(_G.diagnostic_bufnr)
vim.diagnostic.set(_G.diagnostic_ns, _G.diagnostic_bufnr, {
_G.make_error('Diagnostic #1', 2, 3, 3, 4),
})
end)
end)
it('finds next diagnostic at the end of the line', function()
eq(
{ 3, 2 },
exec_lua(function()
vim.api.nvim_win_set_cursor(0, { 3, 0 })
vim.diagnostic.jump({ count = 1 })
return vim.api.nvim_win_get_cursor(0)
end)
)
end)
it('finds previous diagnostic at the end of the line', function()
eq(
{ 3, 2 },
exec_lua(function()
vim.api.nvim_win_set_cursor(0, { 4, 2 })
vim.diagnostic.jump({ count = -1 })
return vim.api.nvim_win_get_cursor(0)
end)
)
end)
end)
end)
describe('get()', function()
@@ -2924,7 +2995,7 @@ describe('vim.diagnostic', function()
local float_bufnr, winnr = vim.diagnostic.open_float({
header = false,
scope = 'cursor',
pos = { 0, first_line_len },
pos = { 0, first_line_len - 1 },
})
local lines = vim.api.nvim_buf_get_lines(float_bufnr, 0, -1, false)
vim.api.nvim_win_close(winnr, true)
@@ -2959,7 +3030,7 @@ describe('vim.diagnostic', function()
vim.diagnostic.set(_G.diagnostic_ns, _G.diagnostic_bufnr, diagnostics)
vim.api.nvim_win_set_cursor(0, { 1, 1 })
local float_bufnr, winnr =
vim.diagnostic.open_float({ header = false, scope = 'cursor', pos = { 2, 1 } })
vim.diagnostic.open_float({ header = false, scope = 'cursor', pos = { 2, 0 } })
local lines = vim.api.nvim_buf_get_lines(float_bufnr, 0, -1, false)
vim.api.nvim_win_close(winnr, true)
return lines
@@ -3564,6 +3635,88 @@ describe('vim.diagnostic', function()
end)
)
end)
it('shows diagnostics at their logical locations after text changes before', function()
exec_lua(function()
vim.api.nvim_set_current_buf(_G.diagnostic_bufnr)
vim.diagnostic.set(_G.diagnostic_ns, _G.diagnostic_bufnr, {
_G.make_error('Diagnostic #1', 1, 4, 1, 7),
_G.make_error('Diagnostic #2', 3, 0, 3, 3),
})
end)
api.nvim_buf_set_text(0, 3, 0, 3, 0, { 'new line', 'new ' })
eq(
{ 'Diagnostic #1' },
exec_lua(function()
vim.api.nvim_win_set_cursor(0, { 2, 4 })
local float_bufnr, winnr = vim.diagnostic.open_float({ header = '', scope = 'cursor' })
local lines = vim.api.nvim_buf_get_lines(float_bufnr, 0, -1, false)
vim.api.nvim_win_close(winnr, true)
return lines
end)
)
eq(
{ 'Diagnostic #2' },
exec_lua(function()
vim.api.nvim_win_set_cursor(0, { 5, 4 })
local float_bufnr, winnr = vim.diagnostic.open_float({ header = '', scope = 'cursor' })
local lines = vim.api.nvim_buf_get_lines(float_bufnr, 0, -1, false)
vim.api.nvim_win_close(winnr, true)
return lines
end)
)
end)
it('shows diagnostics at their logical locations after text changes inside', function()
exec_lua(function()
vim.api.nvim_set_current_buf(_G.diagnostic_bufnr)
vim.diagnostic.set(_G.diagnostic_ns, _G.diagnostic_bufnr, {
_G.make_error('Diagnostic #1', 1, 0, 1, 7),
})
end)
api.nvim_buf_set_text(0, 1, 4, 1, 4, { 'new ' })
eq(
{ 'Diagnostic #1' },
exec_lua(function()
vim.api.nvim_win_set_cursor(0, { 2, 10 })
local float_bufnr, winnr = vim.diagnostic.open_float({ header = '', scope = 'cursor' })
local lines = vim.api.nvim_buf_get_lines(float_bufnr, 0, -1, false)
vim.api.nvim_win_close(winnr, true)
return lines
end)
)
end)
it(
'shows diagnostics at the end of the line if diagnostic is set after last character in line',
function()
exec_lua(function()
vim.api.nvim_set_current_buf(_G.diagnostic_bufnr)
vim.diagnostic.set(_G.diagnostic_ns, _G.diagnostic_bufnr, {
_G.make_error('Diagnostic #1', 2, 3, 3, 4),
})
end)
eq(
{ 'Diagnostic #1' },
exec_lua(function()
vim.api.nvim_win_set_cursor(0, { 3, 2 })
local float_bufnr, winnr = vim.diagnostic.open_float({ header = '', scope = 'cursor' })
local lines = vim.api.nvim_buf_get_lines(float_bufnr, 0, -1, false)
vim.api.nvim_win_close(winnr, true)
return lines
end)
)
end
)
end)
describe('setloclist()', function()
@@ -3840,9 +3993,9 @@ describe('vim.diagnostic', function()
local list = vim.fn.getqflist()
local new_diagnostics = vim.diagnostic.fromqflist(list)
-- Remove namespace since it isn't present in the return value of
-- fromlist()
-- Remove extra properties not present in the return value of fromlist()
for _, v in ipairs(diagnostics) do
v._extmark_id = nil
v.namespace = nil
end