feat(diagnostic): add support for tags

The LSP spec supports two tags that can be added to diagnostics:
unnecessary and deprecated. Extend vim.diagnostic to be able to handle
these.
This commit is contained in:
Lewis Russell
2023-03-30 14:49:58 +01:00
committed by GitHub
parent 8fa7d833cf
commit 226a6c3eae
9 changed files with 83 additions and 16 deletions

View File

@@ -1113,7 +1113,8 @@ code_action({options}) *vim.lsp.buf.code_action()*
• {options} (table|nil) Optional table which holds the following • {options} (table|nil) Optional table which holds the following
optional fields: optional fields:
• context: (table|nil) Corresponds to `CodeActionContext` of the LSP specification: • context: (table|nil) Corresponds to `CodeActionContext` of the LSP specification:
• diagnostics (table|nil): LSP`Diagnostic[]` . Inferred from the current position if not provided. • diagnostics (table|nil): LSP `Diagnostic[]`. Inferred
from the current position if not provided.
• only (table|nil): List of LSP `CodeActionKind`s used to • only (table|nil): List of LSP `CodeActionKind`s used to
filter the code actions. Most language servers support filter the code actions. Most language servers support
values like `refactor` or `quickfix`. values like `refactor` or `quickfix`.

View File

@@ -226,6 +226,9 @@ The following new APIs or features were added.
• Added |nvim_get_hl()| for getting highlight group definitions in a format compatible with |nvim_set_hl()|. • Added |nvim_get_hl()| for getting highlight group definitions in a format compatible with |nvim_set_hl()|.
• |vim.diagnostic| now supports LSP DiagnosticsTag.
See: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticTag
============================================================================== ==============================================================================
CHANGED FEATURES *news-changes* CHANGED FEATURES *news-changes*

View File

@@ -483,6 +483,7 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
local diagnostics = local diagnostics =
get_diagnostics(bufnr, vim.tbl_extend('keep', opts, { namespace = namespace }), true) get_diagnostics(bufnr, vim.tbl_extend('keep', opts, { namespace = namespace }), true)
local line_diagnostics = diagnostic_lines(diagnostics) local line_diagnostics = diagnostic_lines(diagnostics)
for i = 0, line_count do for i = 0, line_count do
local offset = i * (search_forward and 1 or -1) local offset = i * (search_forward and 1 or -1)
local lnum = position[1] + offset local lnum = position[1] + offset
@@ -752,6 +753,7 @@ end
---@field message string ---@field message string
---@field source nil|string ---@field source nil|string
---@field code nil|string ---@field code nil|string
---@field _tags { deprecated: boolean, unnecessary: boolean}
---@field user_data nil|any arbitrary data plugins can add ---@field user_data nil|any arbitrary data plugins can add
--- Get current diagnostics. --- Get current diagnostics.
@@ -948,6 +950,16 @@ M.handlers.underline = {
higroup = underline_highlight_map.Error higroup = underline_highlight_map.Error
end end
if diagnostic._tags then
-- TODO(lewis6991): we should be able to stack these.
if diagnostic._tags.unnecessary then
higroup = 'DiagnosticUnnecessary'
end
if diagnostic._tags.deprecated then
higroup = 'DiagnosticDeprecated'
end
end
vim.highlight.range( vim.highlight.range(
bufnr, bufnr,
underline_ns, underline_ns,

View File

@@ -1,13 +1,6 @@
---@brief lsp-diagnostic ---@brief lsp-diagnostic
---
---@class Diagnostic local protocol = require('vim.lsp.protocol')
---@field range Range
---@field message string
---@field severity DiagnosticSeverity|nil
---@field code integer | string
---@field source string
---@field tags DiagnosticTag[]
---@field relatedInformation DiagnosticRelatedInformation[]
local M = {} local M = {}
@@ -22,14 +15,16 @@ local function get_client_id(client_id)
end end
---@private ---@private
---@param severity lsp.DiagnosticSeverity
local function severity_lsp_to_vim(severity) local function severity_lsp_to_vim(severity)
if type(severity) == 'string' then if type(severity) == 'string' then
severity = vim.lsp.protocol.DiagnosticSeverity[severity] severity = protocol.DiagnosticSeverity[severity]
end end
return severity return severity
end end
---@private ---@private
---@return lsp.DiagnosticSeverity
local function severity_vim_to_lsp(severity) local function severity_vim_to_lsp(severity)
if type(severity) == 'string' then if type(severity) == 'string' then
severity = vim.diagnostic.severity[severity] severity = vim.diagnostic.severity[severity]
@@ -38,6 +33,7 @@ local function severity_vim_to_lsp(severity)
end end
---@private ---@private
---@return integer
local function line_byte_from_position(lines, lnum, col, offset_encoding) local function line_byte_from_position(lines, lnum, col, offset_encoding)
if not lines or offset_encoding == 'utf-8' then if not lines or offset_encoding == 'utf-8' then
return col return col
@@ -77,12 +73,41 @@ local function get_buf_lines(bufnr)
return lines return lines
end end
--- @private
--- @param diagnostic lsp.Diagnostic
--- @param client_id integer
--- @return table?
local function tags_lsp_to_vim(diagnostic, client_id)
local tags ---@type table?
for _, tag in ipairs(diagnostic.tags or {}) do
if tag == protocol.DiagnosticTag.Unnecessary then
tags = tags or {}
tags.unnecessary = true
elseif tag == protocol.DiagnosticTag.Deprecated then
tags = tags or {}
tags.deprecated = true
else
vim.notify_once(
string.format('Unknown DiagnosticTag %d from LSP client %d', tag, client_id),
vim.log.levels.WARN
)
end
end
return tags
end
---@private ---@private
---@param diagnostics lsp.Diagnostic[]
---@param bufnr integer
---@param client_id integer
---@return Diagnostic[]
local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id) local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
local buf_lines = get_buf_lines(bufnr) local buf_lines = get_buf_lines(bufnr)
local client = vim.lsp.get_client_by_id(client_id) local client = vim.lsp.get_client_by_id(client_id)
local offset_encoding = client and client.offset_encoding or 'utf-16' local offset_encoding = client and client.offset_encoding or 'utf-16'
---@diagnostic disable-next-line:no-unknown
return vim.tbl_map(function(diagnostic) return vim.tbl_map(function(diagnostic)
---@cast diagnostic lsp.Diagnostic
local start = diagnostic.range.start local start = diagnostic.range.start
local _end = diagnostic.range['end'] local _end = diagnostic.range['end']
return { return {
@@ -94,12 +119,12 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
message = diagnostic.message, message = diagnostic.message,
source = diagnostic.source, source = diagnostic.source,
code = diagnostic.code, code = diagnostic.code,
tags = tags_lsp_to_vim(diagnostic, client_id),
user_data = { user_data = {
lsp = { lsp = {
-- usage of user_data.lsp.code is deprecated in favor of the top-level code field -- usage of user_data.lsp.code is deprecated in favor of the top-level code field
code = diagnostic.code, code = diagnostic.code,
codeDescription = diagnostic.codeDescription, codeDescription = diagnostic.codeDescription,
tags = diagnostic.tags,
relatedInformation = diagnostic.relatedInformation, relatedInformation = diagnostic.relatedInformation,
data = diagnostic.data, data = diagnostic.data,
}, },
@@ -108,9 +133,13 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
end, diagnostics) end, diagnostics)
end end
---@private --- @private
--- @param diagnostics Diagnostic[]
--- @return lsp.Diagnostic[]
local function diagnostic_vim_to_lsp(diagnostics) local function diagnostic_vim_to_lsp(diagnostics)
---@diagnostic disable-next-line:no-unknown
return vim.tbl_map(function(diagnostic) return vim.tbl_map(function(diagnostic)
---@cast diagnostic Diagnostic
return vim.tbl_extend('keep', { return vim.tbl_extend('keep', {
-- "keep" the below fields over any duplicate fields in diagnostic.user_data.lsp -- "keep" the below fields over any duplicate fields in diagnostic.user_data.lsp
range = { range = {
@@ -131,6 +160,7 @@ local function diagnostic_vim_to_lsp(diagnostics)
end, diagnostics) end, diagnostics)
end end
---@type table<integer,integer>
local _client_namespaces = {} local _client_namespaces = {}
--- Get the diagnostic namespace associated with an LSP client |vim.diagnostic|. --- Get the diagnostic namespace associated with an LSP client |vim.diagnostic|.

View File

@@ -21,6 +21,7 @@ end
--]=] --]=]
local constants = { local constants = {
--- @enum lsp.DiagnosticSeverity
DiagnosticSeverity = { DiagnosticSeverity = {
-- Reports an error. -- Reports an error.
Error = 1, Error = 1,
@@ -32,6 +33,7 @@ local constants = {
Hint = 4, Hint = 4,
}, },
--- @enum lsp.DiagnosticTag
DiagnosticTag = { DiagnosticTag = {
-- Unused or unnecessary code -- Unused or unnecessary code
Unnecessary = 1, Unnecessary = 1,

View File

@@ -18,3 +18,20 @@
---@class lsp.FileEvent ---@class lsp.FileEvent
---@field uri string ---@field uri string
---@field type lsp.FileChangeType ---@field type lsp.FileChangeType
---@class lsp.Position
---@field line integer
---@field character integer
---@class lsp.Range
---@field start lsp.Position
---@field end lsp.Position
---@class lsp.Diagnostic
---@field range lsp.Range
---@field message string
---@field severity? lsp.DiagnosticSeverity
---@field code integer | string
---@field source string
---@field tags? lsp.DiagnosticTag[]
---@field relatedInformation DiagnosticRelatedInformation[]

View File

@@ -218,6 +218,8 @@ static const char *highlight_init_both[] = {
"default link DiagnosticSignInfo DiagnosticInfo", "default link DiagnosticSignInfo DiagnosticInfo",
"default link DiagnosticSignHint DiagnosticHint", "default link DiagnosticSignHint DiagnosticHint",
"default link DiagnosticSignOk DiagnosticOk", "default link DiagnosticSignOk DiagnosticOk",
"default DiagnosticDeprecated cterm=strikethrough gui=strikethrough guisp=Red",
"default link DiagnosticUnnecessary Comment",
// Text // Text
"default link @text.literal Comment", "default link @text.literal Comment",

View File

@@ -86,6 +86,7 @@ describe('vim.diagnostic', function()
it('creates highlight groups', function() it('creates highlight groups', function()
command('runtime plugin/diagnostic.vim') command('runtime plugin/diagnostic.vim')
eq({ eq({
'DiagnosticDeprecated',
'DiagnosticError', 'DiagnosticError',
'DiagnosticFloatingError', 'DiagnosticFloatingError',
'DiagnosticFloatingHint', 'DiagnosticFloatingHint',
@@ -105,6 +106,7 @@ describe('vim.diagnostic', function()
'DiagnosticUnderlineInfo', 'DiagnosticUnderlineInfo',
'DiagnosticUnderlineOk', 'DiagnosticUnderlineOk',
'DiagnosticUnderlineWarn', 'DiagnosticUnderlineWarn',
'DiagnosticUnnecessary',
'DiagnosticVirtualTextError', 'DiagnosticVirtualTextError',
'DiagnosticVirtualTextHint', 'DiagnosticVirtualTextHint',
'DiagnosticVirtualTextInfo', 'DiagnosticVirtualTextInfo',

View File

@@ -97,7 +97,6 @@ describe('vim.lsp.diagnostic', function()
} }
diagnostics[1].code = 42 diagnostics[1].code = 42
diagnostics[1].tags = {"foo", "bar"}
diagnostics[1].data = "Hello world" diagnostics[1].data = "Hello world"
vim.lsp.diagnostic.on_publish_diagnostics(nil, { vim.lsp.diagnostic.on_publish_diagnostics(nil, {
@@ -110,10 +109,9 @@ describe('vim.lsp.diagnostic', function()
vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1)[1], vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1)[1],
} }
]] ]]
eq({code = 42, tags = {"foo", "bar"}, data = "Hello world"}, result[1].user_data.lsp) eq({code = 42, data = "Hello world"}, result[1].user_data.lsp)
eq(42, result[1].code) eq(42, result[1].code)
eq(42, result[2].code) eq(42, result[2].code)
eq({"foo", "bar"}, result[2].tags)
eq("Hello world", result[2].data) eq("Hello world", result[2].data)
end) end)
end) end)