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: +---
+--- 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})
+---
+---
+---@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)