Skip to content

Commit

Permalink
feat(diagnostic): match(), tolist(), fromlist() neovim#15704
Browse files Browse the repository at this point in the history
* feat(diagnostic): add vim.diagnostic.match()
  Provide vim.diagnostic.match() to generate a diagnostic from a string and
  a Lua pattern.
* feat(diagnostic): add tolist() and fromlist()
  • Loading branch information
gpanders committed Sep 19, 2021
1 parent 853346a commit e61ea77
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 33 deletions.
1 change: 1 addition & 0 deletions runtime/doc/deprecated.txt
Expand Up @@ -96,6 +96,7 @@ internally and are no longer exposed as part of the API. Instead, use

LSP Utility Functions ~

*vim.lsp.util.diagnostics_to_items()* Use |vim.diagnostic.toqflist()| instead.
*vim.lsp.util.set_qflist()* Use |setqflist()| instead.
*vim.lsp.util.set_loclist()* Use |setloclist()| instead.

Expand Down
60 changes: 58 additions & 2 deletions runtime/doc/diagnostic.txt
Expand Up @@ -182,8 +182,8 @@ is the first letter of the severity name (for example, "E" for ERROR). Signs
can be customized using the following: >
sign define DiagnosticSignError text=E texthl=DiagnosticSignError linehl= numhl=
sign define DiagnosticSignWarning text=W texthl=DiagnosticSignWarning linehl= numhl=
sign define DiagnosticSignInformation text=I texthl=DiagnosticSignInformation linehl= numhl=
sign define DiagnosticSignWarn text=W texthl=DiagnosticSignWarn linehl= numhl=
sign define DiagnosticSignInfo text=I texthl=DiagnosticSignInfo linehl= numhl=
sign define DiagnosticSignHint text=H texthl=DiagnosticSignHint linehl= numhl=
==============================================================================
Expand Down Expand Up @@ -265,6 +265,16 @@ enable({bufnr}, {namespace}) *vim.diagnostic.enable()*
{namespace} number|nil Only enable diagnostics for the
given namespace.

fromqflist({list}) *vim.diagnostic.fromqflist()*
Convert a list of quickfix items to a list of diagnostics.

Parameters: ~
{list} table A list of quickfix items from |getqflist()|
or |getloclist()|.

Return: ~
array of diagnostics |diagnostic-structure|

get({bufnr}, {opts}) *vim.diagnostic.get()*
Get current diagnostics.

Expand Down Expand Up @@ -384,6 +394,41 @@ hide({namespace}, {bufnr}) *vim.diagnostic.hide()*
{bufnr} number|nil Buffer number. Defaults to the
current buffer.

*vim.diagnostic.match()*
match({str}, {pat}, {groups}, {severity_map}, {defaults})
Parse a diagnostic from a string.

For example, consider a line of output from a linter: >
WARNING filename:27:3: Variable 'foo' does not exist
< This can be parsed into a diagnostic |diagnostic-structure|
with: >
local s = "WARNING filename:27:3: Variable 'foo' does not exist"
local pattern = "^(%w+) %w+:(%d+):(%d+): (.+)$"
local groups = {"severity", "lnum", "col", "message"}
vim.diagnostic.match(s, pattern, groups, {WARNING = vim.diagnostic.WARN})
<

Parameters: ~
{str} string String to parse diagnostics from.
{pat} string Lua pattern with capture groups.
{groups} table List of fields in a
|diagnostic-structure| to associate with
captures from {pat}.
{severity_map} table A table mapping the severity field
from {groups} with an item from
|vim.diagnostic.severity|.
{defaults} table|nil Table of default values for any
fields not listed in {groups}. When
omitted, numeric values default to 0 and
"severity" defaults to ERROR.

Return: ~
diagnostic |diagnostic-structure| or `nil` if {pat} fails
to match {str}.

reset({namespace}, {bufnr}) *vim.diagnostic.reset()*
Remove all diagnostics from the given namespace.

Expand Down Expand Up @@ -495,4 +540,15 @@ show_position_diagnostics({opts}, {bufnr}, {position})
Return: ~
tuple ({popup_bufnr}, {win_id})

toqflist({diagnostics}) *vim.diagnostic.toqflist()*
Convert a list of diagnostics to a list of quickfix items that
can be passed to |setqflist()| or |setloclist()|.

Parameters: ~
{diagnostics} table List of diagnostics
|diagnostic-structure|.

Return: ~
array of quickfix list items |setqflist-what|

vim:tw=78:ts=8:ft=help:norl:
166 changes: 135 additions & 31 deletions runtime/lua/vim/diagnostic.lua
Expand Up @@ -9,6 +9,12 @@ M.severity = {

vim.tbl_add_reverse_lookup(M.severity)

-- Mappings from qflist/loclist error types to severities
M.severity.E = M.severity.ERROR
M.severity.W = M.severity.WARN
M.severity.I = M.severity.INFO
M.severity.N = M.severity.HINT

local global_diagnostic_options = {
signs = true,
underline = true,
Expand Down Expand Up @@ -375,35 +381,6 @@ local function show_diagnostics(opts, diagnostics)
return popup_bufnr, winnr
end

local errlist_type_map = {
[M.severity.ERROR] = 'E',
[M.severity.WARN] = 'W',
[M.severity.INFO] = 'I',
[M.severity.HINT] = 'I',
}

---@private
local function diagnostics_to_list_items(diagnostics)
local items = {}
for _, d in pairs(diagnostics) do
table.insert(items, {
bufnr = d.bufnr,
lnum = d.lnum + 1,
col = d.col + 1,
text = d.message,
type = errlist_type_map[d.severity or M.severity.ERROR] or 'E'
})
end
table.sort(items, function(a, b)
if a.bufnr == b.bufnr then
return a.lnum < b.lnum
else
return a.bufnr < b.bufnr
end
end)
return items
end

---@private
local function set_list(loclist, opts)
opts = opts or {}
Expand All @@ -415,7 +392,7 @@ local function set_list(loclist, opts)
bufnr = vim.api.nvim_win_get_buf(winnr)
end
local diagnostics = M.get(bufnr, opts)
local items = diagnostics_to_list_items(diagnostics)
local items = M.toqflist(diagnostics)
if loclist then
vim.fn.setloclist(winnr, {}, ' ', { title = title, items = items })
else
Expand Down Expand Up @@ -1168,7 +1145,134 @@ function M.enable(bufnr, namespace)
end
end

-- }}}
--- Parse a diagnostic from a string.
---
--- For example, consider a line of output from a linter:
--- <pre>
--- WARNING filename:27:3: Variable 'foo' does not exist
--- </pre>
--- This can be parsed into a diagnostic |diagnostic-structure|
--- with:
--- <pre>
--- local s = "WARNING filename:27:3: Variable 'foo' does not exist"
--- local pattern = "^(%w+) %w+:(%d+):(%d+): (.+)$"
--- local groups = {"severity", "lnum", "col", "message"}
--- vim.diagnostic.match(s, pattern, groups, {WARNING = vim.diagnostic.WARN})
--- </pre>
---
---@param str string String to parse diagnostics from.
---@param pat string Lua pattern with capture groups.
---@param groups table List of fields in a |diagnostic-structure| to
--- associate with captures from {pat}.
---@param severity_map table A table mapping the severity field from {groups}
--- with an item from |vim.diagnostic.severity|.
---@param defaults table|nil Table of default values for any fields not listed in {groups}.
--- When omitted, numeric values default to 0 and "severity" defaults to
--- ERROR.
---@return diagnostic |diagnostic-structure| or `nil` if {pat} fails to match {str}.
function M.match(str, pat, groups, severity_map, defaults)
vim.validate {
str = { str, 's' },
pat = { pat, 's' },
groups = { groups, 't' },
severity_map = { severity_map, 't', true },
defaults = { defaults, 't', true },
}

severity_map = severity_map or M.severity

local diagnostic = {}
local matches = {string.match(str, pat)}
if vim.tbl_isempty(matches) then
return
end

for i, match in ipairs(matches) do
local field = groups[i]
if field == "severity" then
match = severity_map[match]
elseif field == "lnum" or field == "end_lnum" or field == "col" or field == "end_col" then
match = assert(tonumber(match)) - 1
end
diagnostic[field] = match
end

diagnostic = vim.tbl_extend("keep", diagnostic, defaults or {})
diagnostic.severity = diagnostic.severity or M.severity.ERROR
diagnostic.col = diagnostic.col or 0
diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum
diagnostic.end_col = diagnostic.end_col or diagnostic.col
return diagnostic
end

local errlist_type_map = {
[M.severity.ERROR] = 'E',
[M.severity.WARN] = 'W',
[M.severity.INFO] = 'I',
[M.severity.HINT] = 'N',
}

--- Convert a list of diagnostics to a list of quickfix items that can be
--- passed to |setqflist()| or |setloclist()|.
---
---@param diagnostics table List of diagnostics |diagnostic-structure|.
---@return array of quickfix list items |setqflist-what|
function M.toqflist(diagnostics)
vim.validate { diagnostics = {diagnostics, 't'} }

local list = {}
for _, v in ipairs(diagnostics) do
local item = {
bufnr = v.bufnr,
lnum = v.lnum + 1,
col = v.col and (v.col + 1) or nil,
end_lnum = v.end_lnum and (v.end_lnum + 1) or nil,
end_col = v.end_col and (v.end_col + 1) or nil,
text = v.message,
type = errlist_type_map[v.severity] or 'E',
}
table.insert(list, item)
end
table.sort(list, function(a, b)
if a.bufnr == b.bufnr then
return a.lnum < b.lnum
else
return a.bufnr < b.bufnr
end
end)
return list
end

--- Convert a list of quickfix items to a list of diagnostics.
---
---@param list table A list of quickfix items from |getqflist()| or
--- |getloclist()|.
---@return array of diagnostics |diagnostic-structure|
function M.fromqflist(list)
vim.validate { list = {list, 't'} }

local diagnostics = {}
for _, item in ipairs(list) do
if item.valid == 1 then
local lnum = math.max(0, item.lnum - 1)
local col = item.col > 0 and (item.col - 1) or nil
local end_lnum = item.end_lnum > 0 and (item.end_lnum - 1) or lnum
local end_col = item.end_col > 0 and (item.end_col - 1) or col
local severity = item.type ~= "" and M.severity[item.type] or M.severity.ERROR
table.insert(diagnostics, {
bufnr = item.bufnr,
lnum = lnum,
col = col,
end_lnum = end_lnum,
end_col = end_col,
severity = severity,
message = item.text,
})
end
end
return diagnostics
end

-- }}}

return M
81 changes: 81 additions & 0 deletions test/functional/lua/diagnostic_spec.lua
@@ -1,5 +1,6 @@
local helpers = require('test.functional.helpers')(after_each)

local NIL = helpers.NIL
local command = helpers.command
local clear = helpers.clear
local exec_lua = helpers.exec_lua
Expand Down Expand Up @@ -912,4 +913,84 @@ describe('vim.diagnostic', function()
assert(loc_list[1].lnum < loc_list[2].lnum)
end)
end)

describe('match()', function()
it('matches a string', function()
local msg = "ERROR: george.txt:19:84:Two plus two equals five"
local diagnostic = {
severity = exec_lua [[return vim.diagnostic.severity.ERROR]],
lnum = 18,
col = 83,
end_lnum = 18,
end_col = 83,
message = "Two plus two equals five",
}
eq(diagnostic, exec_lua([[
return vim.diagnostic.match(..., "^(%w+): [^:]+:(%d+):(%d+):(.+)$", {"severity", "lnum", "col", "message"})
]], msg))
end)

it('returns nil if the pattern fails to match', function()
eq(NIL, exec_lua [[
local msg = "The answer to life, the universe, and everything is"
return vim.diagnostic.match(msg, "This definitely will not match", {})
]])
end)

it('respects default values', function()
local msg = "anna.txt:1:Happy families are all alike"
local diagnostic = {
severity = exec_lua [[return vim.diagnostic.severity.INFO]],
lnum = 0,
col = 0,
end_lnum = 0,
end_col = 0,
message = "Happy families are all alike",
}
eq(diagnostic, exec_lua([[
return vim.diagnostic.match(..., "^[^:]+:(%d+):(.+)$", {"lnum", "message"}, nil, {severity = vim.diagnostic.severity.INFO})
]], msg))
end)

it('accepts a severity map', function()
local msg = "46:FATAL:Et tu, Brute?"
local diagnostic = {
severity = exec_lua [[return vim.diagnostic.severity.ERROR]],
lnum = 45,
col = 0,
end_lnum = 45,
end_col = 0,
message = "Et tu, Brute?",
}
eq(diagnostic, exec_lua([[
return vim.diagnostic.match(..., "^(%d+):(%w+):(.+)$", {"lnum", "severity", "message"}, {FATAL = vim.diagnostic.severity.ERROR})
]], msg))
end)
end)

describe('toqflist() and fromqflist()', function()
it('works', function()
local result = exec_lua [[
vim.diagnostic.set(diagnostic_ns, diagnostic_bufnr, {
make_error('Error 1', 0, 1, 0, 1),
make_error('Error 2', 1, 1, 1, 1),
make_warning('Warning', 2, 2, 2, 2),
})
local diagnostics = vim.diagnostic.get(diagnostic_bufnr)
vim.fn.setqflist(vim.diagnostic.toqflist(diagnostics))
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()
for _, v in ipairs(diagnostics) do
v.namespace = nil
end
return {diagnostics, new_diagnostics}
]]
eq(result[1], result[2])
end)
end)
end)

0 comments on commit e61ea77

Please sign in to comment.