aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregory Anders <greg@gpanders.com>2021-09-19 16:13:23 -0600
committerGitHub <noreply@github.com>2021-09-19 15:13:23 -0700
commite61ea7772e5eab2d0460dae858698f16b0ee8f27 (patch)
tree4991ac500046d80bb9cd28e3df7536c05f7d04cd
parent853346a94d6aa78c97314a3b217fb5a5408a47f1 (diff)
downloadrneovim-e61ea7772e5eab2d0460dae858698f16b0ee8f27.tar.gz
rneovim-e61ea7772e5eab2d0460dae858698f16b0ee8f27.tar.bz2
rneovim-e61ea7772e5eab2d0460dae858698f16b0ee8f27.zip
feat(diagnostic): match(), tolist(), fromlist() #15704
* 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()
-rw-r--r--runtime/doc/deprecated.txt1
-rw-r--r--runtime/doc/diagnostic.txt60
-rw-r--r--runtime/lua/vim/diagnostic.lua166
-rw-r--r--test/functional/lua/diagnostic_spec.lua81
4 files changed, 275 insertions, 33 deletions
diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt
index d1f26c8c81..a0c291964e 100644
--- a/runtime/doc/deprecated.txt
+++ b/runtime/doc/deprecated.txt
@@ -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.
diff --git a/runtime/doc/diagnostic.txt b/runtime/doc/diagnostic.txt
index 199c04be98..59b73771a6 100644
--- a/runtime/doc/diagnostic.txt
+++ b/runtime/doc/diagnostic.txt
@@ -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=
==============================================================================
@@ -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.
@@ -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.
@@ -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:
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index 61311f2135..c977fe0109 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -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,
@@ -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 {}
@@ -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
@@ -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
diff --git a/test/functional/lua/diagnostic_spec.lua b/test/functional/lua/diagnostic_spec.lua
index 589e0f3e6b..29dd5c60da 100644
--- a/test/functional/lua/diagnostic_spec.lua
+++ b/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
@@ -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)