Skip to content

Commit

Permalink
feat(lsp): jump to diagnostics by position (#14795)
Browse files Browse the repository at this point in the history
  • Loading branch information
pohzipohzi committed Aug 19, 2021
1 parent d088066 commit ea39ff5
Showing 1 changed file with 118 additions and 70 deletions.
188 changes: 118 additions & 70 deletions runtime/lua/vim/lsp/diagnostic.lua
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,12 @@ end
---@param bufnr number
---@param client_id number|nil If nil, then return all of the diagnostics.
--- 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
local all_diagnostics = {}
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
table.insert(all_diagnostics, diagnostic)
Expand All @@ -376,19 +377,26 @@ function M.get(bufnr, client_id)
return all_diagnostics
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

--- Get the diagnostics by line
---
---@param bufnr number The buffer number
---@param line_nr number The line number
---@param bufnr number|nil The buffer number
---@param line_nr number|nil The line number
---@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.
---@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.
-- Structured: { [1] = {...}, [5] = {.... } }
function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
Expand Down Expand Up @@ -464,63 +472,64 @@ end
-- }}}
-- Diagnostic Movements {{{

--- Helper function to iterate through all of the diagnostic lines
---@return table list of diagnostics
local _iter_diagnostic_lines = function(start, finish, step, bufnr, opts, client_id)
if bufnr == nil then
bufnr = vim.api.nvim_get_current_buf()
end

--- Helper function to find the next diagnostic relative to a position
---@return table the next diagnostic if found
local _next_diagnostic = function(position, search_forward, bufnr, opts, client_id)
position[1] = position[1] - 1
bufnr = bufnr or vim.api.nvim_get_current_buf()
local wrap = if_nil(opts.wrap, true)

local search = function(search_start, search_finish, search_step)
for line_nr = search_start, search_finish, search_step do
local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then
return line_diagnostics
local line_count = vim.api.nvim_buf_line_count(bufnr)
for i = 0, line_count 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
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
wrap_start, wrap_finish = vim.api.nvim_buf_line_count(bufnr), start
end

if not result then
result = search(wrap_start, wrap_finish, step)
local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then
local sort_diagnostics, is_next
if search_forward then
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
else
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
end

return result
end

--@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}
local function _iter_diagnostic_lines_pos(opts, line_diagnostics)
local function _diagnostic_pos(opts, diagnostic)
opts = opts or {}

local win_id = opts.win_id or vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(win_id)

if line_diagnostics == nil or vim.tbl_isempty(line_diagnostics) then
return false
end
if not diagnostic then return false end

local iter_diagnostic = line_diagnostics[1]
return to_position(iter_diagnostic.range.start, bufnr)
return to_position(diagnostic.range.start, bufnr)
end

--@private
-- 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 {}

local enable_popup = if_nil(opts.enable_popup, true)
Expand All @@ -536,7 +545,7 @@ local function _iter_diagnostic_move_pos(name, opts, pos)
if enable_popup then
-- This is a bit weird... I'm surprised that we need to wait til the next tick to do this.
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
Expand All @@ -552,14 +561,14 @@ function M.get_prev(opts)
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)

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

--- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic position
function M.get_prev_pos(opts)
return _iter_diagnostic_lines_pos(
return _diagnostic_pos(
opts,
M.get_prev(opts)
)
Expand All @@ -568,7 +577,7 @@ end
--- Move to the previous diagnostic
---@param opts table See |vim.lsp.diagnostic.goto_next()|
function M.goto_prev(opts)
return _iter_diagnostic_move_pos(
return _diagnostic_move_pos(
"DiagnosticPrevious",
opts,
M.get_prev_pos(opts)
Expand All @@ -585,14 +594,14 @@ function M.get_next(opts)
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)

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

--- Return the pos, {row, col}, for the next diagnostic in the current buffer.
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic position
function M.get_next_pos(opts)
return _iter_diagnostic_lines_pos(
return _diagnostic_pos(
opts,
M.get_next(opts)
)
Expand All @@ -617,7 +626,7 @@ end
--- - {win_id}: (number, default 0)
--- - Window ID
function M.goto_next(opts)
return _iter_diagnostic_move_pos(
return _diagnostic_move_pos(
"DiagnosticNext",
opts,
M.get_next_pos(opts)
Expand Down Expand Up @@ -1208,7 +1217,7 @@ end
-- }}}
-- 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:
--- <pre>
Expand All @@ -1218,32 +1227,21 @@ end
--- LspDiagnosticsFloatingHint
--- </pre>
---@param opts table Configuration table
--- - show_header (boolean, default true): Show "Diagnostics:" header.
--- - Plus all the opts for |vim.lsp.diagnostic.get_line_diagnostics()|
--- and |vim.lsp.util.open_floating_preview()| can be used here.
---@param bufnr number The buffer number
---@param line_nr number The line number
---@param client_id number|nil the client id
--- - show_header (boolean, default true): Show "Diagnostics:" header
--- - all opts for |vim.lsp.util.open_floating_preview()| can be used here
---@param diagnostics table: The diagnostics to display
---@return table {popup_bufnr, win_id}
function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
opts = opts or {}

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 function show_diagnostics(opts, diagnostics)
if vim.tbl_isempty(diagnostics) then return end
local lines = {}
local highlights = {}
local show_header = if_nil(opts.show_header, true)
if show_header then
table.insert(lines, "Diagnostics:")
table.insert(highlights, {0, "Bold"})
end

local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
if vim.tbl_isempty(line_diagnostics) then return end

for i, diagnostic in ipairs(line_diagnostics) do
for i, diagnostic in ipairs(diagnostics) do
local prefix = string.format("%d. ", i)
local hiname = M._get_floating_severity_highlight_name(diagnostic.severity)
assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
Expand All @@ -1257,7 +1255,6 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
end
end

opts.focus_id = "line_diagnostics"
local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts)
for i, hi in ipairs(highlights) do
local prefixlen, hiname = unpack(hi)
Expand All @@ -1268,6 +1265,57 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
return popup_bufnr, winnr
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
---
Expand Down

0 comments on commit ea39ff5

Please sign in to comment.
-