feat(lsp): jump to diagnostics by position (#14795)

This commit is contained in:
Zi How Poh 2021-08-19 23:36:01 +08:00 committed by GitHub
parent d088066fa1
commit ea39ff5732
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -362,11 +362,12 @@ end
---@param bufnr number ---@param bufnr number
---@param client_id number|nil If nil, then return all of the diagnostics. ---@param client_id number|nil If nil, then return all of the diagnostics.
--- Else, return just the diagnostics associated with the client_id. --- Else, return just the diagnostics associated with the client_id.
function M.get(bufnr, client_id) ---@param predicate function|nil Optional function for filtering diagnostics
function M.get(bufnr, client_id, predicate)
if client_id == nil then if client_id == nil then
local all_diagnostics = {} local all_diagnostics = {}
for iter_client_id, _ in pairs(diagnostic_cache[bufnr]) do for iter_client_id, _ in pairs(diagnostic_cache[bufnr]) do
local iter_diagnostics = M.get(bufnr, iter_client_id) local iter_diagnostics = M.get(bufnr, iter_client_id, predicate)
for _, diagnostic in ipairs(iter_diagnostics) do for _, diagnostic in ipairs(iter_diagnostics) do
table.insert(all_diagnostics, diagnostic) table.insert(all_diagnostics, diagnostic)
@ -376,19 +377,26 @@ function M.get(bufnr, client_id)
return all_diagnostics return all_diagnostics
end end
return diagnostic_cache[bufnr][client_id] or {} predicate = predicate or function(_) return true end
local client_diagnostics = {}
for _, diagnostic in ipairs(diagnostic_cache[bufnr][client_id] or {}) do
if predicate(diagnostic) then
table.insert(client_diagnostics, diagnostic)
end
end
return client_diagnostics
end end
--- Get the diagnostics by line --- Get the diagnostics by line
--- ---
---@param bufnr number The buffer number ---@param bufnr number|nil The buffer number
---@param line_nr number The line number ---@param line_nr number|nil The line number
---@param opts table|nil Configuration keys ---@param opts table|nil Configuration keys
--- - severity: (DiagnosticSeverity, default nil) --- - severity: (DiagnosticSeverity, default nil)
--- - Only return diagnostics with this severity. Overrides severity_limit --- - Only return diagnostics with this severity. Overrides severity_limit
--- - severity_limit: (DiagnosticSeverity, default nil) --- - severity_limit: (DiagnosticSeverity, default nil)
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. --- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---@param client_id number the client id ---@param client_id|nil number the client id
---@return table Table with map of line number to list of diagnostics. ---@return table Table with map of line number to list of diagnostics.
-- Structured: { [1] = {...}, [5] = {.... } } -- Structured: { [1] = {...}, [5] = {.... } }
function M.get_line_diagnostics(bufnr, line_nr, opts, client_id) function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
@ -464,63 +472,64 @@ end
-- }}} -- }}}
-- Diagnostic Movements {{{ -- Diagnostic Movements {{{
--- Helper function to iterate through all of the diagnostic lines --- Helper function to find the next diagnostic relative to a position
---@return table list of diagnostics ---@return table the next diagnostic if found
local _iter_diagnostic_lines = function(start, finish, step, bufnr, opts, client_id) local _next_diagnostic = function(position, search_forward, bufnr, opts, client_id)
if bufnr == nil then position[1] = position[1] - 1
bufnr = vim.api.nvim_get_current_buf() bufnr = bufnr or vim.api.nvim_get_current_buf()
end
local wrap = if_nil(opts.wrap, true) local wrap = if_nil(opts.wrap, true)
local line_count = vim.api.nvim_buf_line_count(bufnr)
local search = function(search_start, search_finish, search_step) for i = 0, line_count do
for line_nr = search_start, search_finish, search_step do local offset = i * (search_forward and 1 or -1)
local line_nr = position[1] + offset
if line_nr < 0 or line_nr >= line_count then
if not wrap then
return
end
line_nr = (line_nr + line_count) % line_count
end
local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id) local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then
return line_diagnostics local sort_diagnostics, is_next
end if search_forward then
end sort_diagnostics = function(a, b) return a.range.start.character < b.range.start.character end
end is_next = function(diagnostic) return diagnostic.range.start.character > position[2] end
local result = search(start, finish, step)
if wrap then
local wrap_start, wrap_finish
if step == 1 then
wrap_start, wrap_finish = 1, start
else else
wrap_start, wrap_finish = vim.api.nvim_buf_line_count(bufnr), start sort_diagnostics = function(a, b) return a.range.start.character > b.range.start.character end
is_next = function(diagnostic) return diagnostic.range.start.character < position[2] end
end
table.sort(line_diagnostics, sort_diagnostics)
if i == 0 then
for _, v in pairs(line_diagnostics) do
if is_next(v) then
return v
end
end
else
return line_diagnostics[1]
end end
if not result then
result = search(wrap_start, wrap_finish, step)
end end
end end
return result
end end
--@private --@private
--- Helper function to ierate through diagnostic lines and return a position --- Helper function to return a position from a diagnostic
--- ---
---@return table {row, col} ---@return table {row, col}
local function _iter_diagnostic_lines_pos(opts, line_diagnostics) local function _diagnostic_pos(opts, diagnostic)
opts = opts or {} opts = opts or {}
local win_id = opts.win_id or vim.api.nvim_get_current_win() local win_id = opts.win_id or vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(win_id) local bufnr = vim.api.nvim_win_get_buf(win_id)
if line_diagnostics == nil or vim.tbl_isempty(line_diagnostics) then if not diagnostic then return false end
return false
end
local iter_diagnostic = line_diagnostics[1] return to_position(diagnostic.range.start, bufnr)
return to_position(iter_diagnostic.range.start, bufnr)
end end
--@private --@private
-- Move to the diagnostic position -- Move to the diagnostic position
local function _iter_diagnostic_move_pos(name, opts, pos) local function _diagnostic_move_pos(name, opts, pos)
opts = opts or {} opts = opts or {}
local enable_popup = if_nil(opts.enable_popup, true) local enable_popup = if_nil(opts.enable_popup, true)
@ -536,7 +545,7 @@ local function _iter_diagnostic_move_pos(name, opts, pos)
if enable_popup then if enable_popup then
-- This is a bit weird... I'm surprised that we need to wait til the next tick to do this. -- This is a bit weird... I'm surprised that we need to wait til the next tick to do this.
vim.schedule(function() vim.schedule(function()
M.show_line_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id)) M.show_position_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id))
end) end)
end end
end end
@ -552,14 +561,14 @@ function M.get_prev(opts)
local bufnr = vim.api.nvim_win_get_buf(win_id) local bufnr = vim.api.nvim_win_get_buf(win_id)
local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id) local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
return _iter_diagnostic_lines(cursor_position[1] - 2, 0, -1, bufnr, opts, opts.client_id) return _next_diagnostic(cursor_position, false, bufnr, opts, opts.client_id)
end end
--- Return the pos, {row, col}, for the prev diagnostic in the current buffer. --- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic position ---@return table Previous diagnostic position
function M.get_prev_pos(opts) function M.get_prev_pos(opts)
return _iter_diagnostic_lines_pos( return _diagnostic_pos(
opts, opts,
M.get_prev(opts) M.get_prev(opts)
) )
@ -568,7 +577,7 @@ end
--- Move to the previous diagnostic --- Move to the previous diagnostic
---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@param opts table See |vim.lsp.diagnostic.goto_next()|
function M.goto_prev(opts) function M.goto_prev(opts)
return _iter_diagnostic_move_pos( return _diagnostic_move_pos(
"DiagnosticPrevious", "DiagnosticPrevious",
opts, opts,
M.get_prev_pos(opts) M.get_prev_pos(opts)
@ -585,14 +594,14 @@ function M.get_next(opts)
local bufnr = vim.api.nvim_win_get_buf(win_id) local bufnr = vim.api.nvim_win_get_buf(win_id)
local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id) local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
return _iter_diagnostic_lines(cursor_position[1], vim.api.nvim_buf_line_count(bufnr), 1, bufnr, opts, opts.client_id) return _next_diagnostic(cursor_position, true, bufnr, opts, opts.client_id)
end end
--- Return the pos, {row, col}, for the next diagnostic in the current buffer. --- Return the pos, {row, col}, for the next diagnostic in the current buffer.
---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic position ---@return table Next diagnostic position
function M.get_next_pos(opts) function M.get_next_pos(opts)
return _iter_diagnostic_lines_pos( return _diagnostic_pos(
opts, opts,
M.get_next(opts) M.get_next(opts)
) )
@ -617,7 +626,7 @@ end
--- - {win_id}: (number, default 0) --- - {win_id}: (number, default 0)
--- - Window ID --- - Window ID
function M.goto_next(opts) function M.goto_next(opts)
return _iter_diagnostic_move_pos( return _diagnostic_move_pos(
"DiagnosticNext", "DiagnosticNext",
opts, opts,
M.get_next_pos(opts) M.get_next_pos(opts)
@ -1208,7 +1217,7 @@ end
-- }}} -- }}}
-- Diagnostic User Functions {{{ -- Diagnostic User Functions {{{
--- Open a floating window with the diagnostics from {line_nr} --- Open a floating window with the provided diagnostics
--- ---
--- The floating window can be customized with the following highlight groups: --- The floating window can be customized with the following highlight groups:
--- <pre> --- <pre>
@ -1218,32 +1227,21 @@ end
--- LspDiagnosticsFloatingHint --- LspDiagnosticsFloatingHint
--- </pre> --- </pre>
---@param opts table Configuration table ---@param opts table Configuration table
--- - show_header (boolean, default true): Show "Diagnostics:" header. --- - show_header (boolean, default true): Show "Diagnostics:" header
--- - Plus all the opts for |vim.lsp.diagnostic.get_line_diagnostics()| --- - all opts for |vim.lsp.util.open_floating_preview()| can be used here
--- and |vim.lsp.util.open_floating_preview()| can be used here. ---@param diagnostics table: The diagnostics to display
---@param bufnr number The buffer number
---@param line_nr number The line number
---@param client_id number|nil the client id
---@return table {popup_bufnr, win_id} ---@return table {popup_bufnr, win_id}
function M.show_line_diagnostics(opts, bufnr, line_nr, client_id) local function show_diagnostics(opts, diagnostics)
opts = opts or {} if vim.tbl_isempty(diagnostics) then return end
local show_header = if_nil(opts.show_header, true)
bufnr = bufnr or 0
line_nr = line_nr or (vim.api.nvim_win_get_cursor(0)[1] - 1)
local lines = {} local lines = {}
local highlights = {} local highlights = {}
local show_header = if_nil(opts.show_header, true)
if show_header then if show_header then
table.insert(lines, "Diagnostics:") table.insert(lines, "Diagnostics:")
table.insert(highlights, {0, "Bold"}) table.insert(highlights, {0, "Bold"})
end end
local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id) for i, diagnostic in ipairs(diagnostics) do
if vim.tbl_isempty(line_diagnostics) then return end
for i, diagnostic in ipairs(line_diagnostics) do
local prefix = string.format("%d. ", i) local prefix = string.format("%d. ", i)
local hiname = M._get_floating_severity_highlight_name(diagnostic.severity) local hiname = M._get_floating_severity_highlight_name(diagnostic.severity)
assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity)) assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
@ -1257,7 +1255,6 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
end end
end end
opts.focus_id = "line_diagnostics"
local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts) local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts)
for i, hi in ipairs(highlights) do for i, hi in ipairs(highlights) do
local prefixlen, hiname = unpack(hi) local prefixlen, hiname = unpack(hi)
@ -1268,6 +1265,57 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
return popup_bufnr, winnr return popup_bufnr, winnr
end end
--- Open a floating window with the diagnostics from {position}
---@param opts table|nil Configuration keys
--- - severity: (DiagnosticSeverity, default nil)
--- - Only return diagnostics with this severity. Overrides severity_limit
--- - severity_limit: (DiagnosticSeverity, default nil)
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
--- - all opts for |show_diagnostics()| can be used here
---@param buf_nr number|nil The buffer number
---@param position table|nil The (0,0)-indexed position
---@return table {popup_bufnr, win_id}
function M.show_position_diagnostics(opts, buf_nr, position)
opts = opts or {}
opts.focus_id = "position_diagnostics"
buf_nr = buf_nr or vim.api.nvim_get_current_buf()
if not position then
local curr_position = vim.api.nvim_win_get_cursor(0)
curr_position[1] = curr_position[1] - 1
position = curr_position
end
local match_position_predicate = function(diag)
return position[1] == diag.range['start'].line and
position[2] >= diag.range['start'].character and
(position[2] <= diag.range['end'].character or position[1] < diag.range['end'].line)
end
local position_diagnostics = M.get(buf_nr, nil, match_position_predicate)
if opts.severity then
position_diagnostics = filter_to_severity_limit(opts.severity, position_diagnostics)
elseif opts.severity_limit then
position_diagnostics = filter_by_severity_limit(opts.severity_limit, position_diagnostics)
end
table.sort(position_diagnostics, function(a, b) return a.severity < b.severity end)
return show_diagnostics(opts, position_diagnostics)
end
--- Open a floating window with the diagnostics from {line_nr}
---@param opts table Configuration table
--- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
--- |show_diagnostics()| can be used here
---@param buf_nr number|nil The buffer number
---@param line_nr number|nil The line number
---@param client_id number|nil the client id
---@return table {popup_bufnr, win_id}
function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
opts = opts or {}
opts.focus_id = "line_diagnostics"
line_nr = line_nr or (vim.api.nvim_win_get_cursor(0)[1] - 1)
local line_diagnostics = M.get_line_diagnostics(buf_nr, line_nr, opts, client_id)
return show_diagnostics(opts, line_diagnostics)
end
--- Clear diagnotics and diagnostic cache --- Clear diagnotics and diagnostic cache
--- ---