From caf780859197ae6e2a3af3f9dd1e79bdca5ef02d Mon Sep 17 00:00:00 2001 From: Oleh Volynets Date: Fri, 13 Mar 2026 12:21:45 +0100 Subject: [PATCH] feat(diagnostic): custom status format function #36696 Problem: Statusline component of diagnostics allows only the default format "sign:count". Solution: Extend vim.diagnostic.Opts.Status to allow a custom signs or formatting function that provides the status presentation. --- runtime/doc/diagnostic.txt | 40 +++++++++++--- runtime/doc/news.txt | 5 ++ runtime/lua/vim/diagnostic.lua | 73 +++++++++++++++++++------ test/functional/lua/diagnostic_spec.lua | 43 ++++++++++++++- 4 files changed, 133 insertions(+), 28 deletions(-) diff --git a/runtime/doc/diagnostic.txt b/runtime/doc/diagnostic.txt index aeafb8e4c6..0792c36d71 100644 --- a/runtime/doc/diagnostic.txt +++ b/runtime/doc/diagnostic.txt @@ -666,9 +666,37 @@ Lua module: vim.diagnostic *diagnostic-api* *vim.diagnostic.Opts.Status* Fields: ~ - • {text}? (`table`) A table mapping - |diagnostic-severity| to the text to use for each severity - section. + • {format}? (`table|(fun(counts:table): string)`) + Either: + • a table mapping |diagnostic-severity| to the text to use + for each existing severity section. + • a function that accepts a mapping of + |diagnostic-severity| to the number of diagnostics of the + corresponding severity (only those severity levels that + have at least 1 diagnostic) and returns a 'statusline' + component. In this case highlights must be applied by the + user in the `format` function. Example: >lua + local signs = { + [vim.diagnostic.severity.ERROR] = "A", + -- ... + } + local hl_map = { + [vim.diagnostic.severity.ERROR] = 'DiagnosticSignError', + -- ... + } + vim.diagnostic.config({ + status = { + format = function(counts) + local items = {} + for level, _ in ipairs(vim.diagnostic.severity) do + local count = counts[level] or 0 + table.insert(items, ("%%#%s#%s %s"):format(hl_map[level], signs[level], count)) + end + return table.concat(items, " ") + end + } + }) +< *vim.diagnostic.Opts.Underline* @@ -1033,11 +1061,7 @@ status({bufnr}) *vim.diagnostic.status()* Returns formatted string with diagnostics for the current buffer. The severities with 0 diagnostics are left out. Example `E:2 W:3 I:4 H:5` - To customise appearance, set diagnostic text for each severity with >lua - vim.diagnostic.config({ - status = { text = { [vim.diagnostic.severity.ERROR] = 'e', ... } } - }) -< + To customise appearance, see |vim.diagnostic.Opts.Status|. Parameters: ~ • {bufnr} (`integer?`) Buffer number to get diagnostics from. Defaults diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index c0a0ebd03f..0247ac7485 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -31,6 +31,11 @@ LSP • |vim.lsp.buf.selection_range()| now accepts an integer which specifies its direction, rather than a string `'outer'` or `'inner'`. +DIAGNOSTICS + +• |vim.diagnostic.Opts.Status|.text was removed in favour of + |vim.diagnostic.Opts.Status|.format. + OPTIONS • `'completefuzzycollect'` and `'isexpand'` have been removed. diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 2f6afcafb6..999558eb9c 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -189,8 +189,37 @@ end --- @class vim.diagnostic.Opts.Status --- ---- A table mapping |diagnostic-severity| to the text to use for each severity section. ---- @field text? table +--- Either: +--- - a table mapping |diagnostic-severity| to the text to use for each +--- existing severity section. +--- - a function that accepts a mapping of |diagnostic-severity| to the +--- number of diagnostics of the corresponding severity (only those +--- severity levels that have at least 1 diagnostic) and returns +--- a 'statusline' component. In this case highlights must be applied +--- by the user in the `format` function. Example: +--- ```lua +--- local signs = { +--- [vim.diagnostic.severity.ERROR] = "A", +--- -- ... +--- } +--- local hl_map = { +--- [vim.diagnostic.severity.ERROR] = 'DiagnosticSignError', +--- -- ... +--- } +--- vim.diagnostic.config({ +--- status = { +--- format = function(counts) +--- local items = {} +--- for level, _ in ipairs(vim.diagnostic.severity) do +--- local count = counts[level] or 0 +--- table.insert(items, ("%%#%s#%s %s"):format(hl_map[level], signs[level], count)) +--- end +--- return table.concat(items, " ") +--- end +--- } +--- }) +--- ``` +--- @field format? table|(fun(counts:table): string) --- @class vim.diagnostic.Opts.Underline --- @@ -2969,17 +2998,19 @@ local hl_map = { [M.severity.INFO] = 'DiagnosticSignInfo', [M.severity.HINT] = 'DiagnosticSignHint', } +local default_status_signs = { + [M.severity.ERROR] = 'E', + [M.severity.WARN] = 'W', + [M.severity.INFO] = 'I', + [M.severity.HINT] = 'H', +} --- Returns formatted string with diagnostics for the current buffer. --- The severities with 0 diagnostics are left out. --- Example `E:2 W:3 I:4 H:5` --- ---- To customise appearance, set diagnostic text for each severity with ---- ```lua ---- vim.diagnostic.config({ ---- status = { text = { [vim.diagnostic.severity.ERROR] = 'e', ... } } ---- }) ---- ``` +--- To customise appearance, see |vim.diagnostic.Opts.Status|. +--- ---@param bufnr? integer Buffer number to get diagnostics from. --- Defaults to 0 for the current buffer --- @@ -2987,20 +3018,26 @@ local hl_map = { function M.status(bufnr) vim.validate('bufnr', bufnr, 'number', true) bufnr = bufnr or 0 + local config = assert(M.config()).status or {} + vim.validate('config.format', config.format, { 'table', 'function' }, true) local counts = M.count(bufnr) - local user_signs = vim.tbl_get(M.config() --[[@as vim.diagnostic.Opts]], 'status', 'text') or {} - local signs = vim.tbl_extend('keep', user_signs, { 'E', 'W', 'I', 'H' }) - local result_str = vim - .iter(pairs(counts)) - :map(function(severity, count) - return ('%%#%s#%s:%s'):format(hl_map[severity], signs[severity], count) - end) - :join(' ') - + local format = config.format or default_status_signs + --- @type string + local result_str + if type(format) == 'table' then + local signs = vim.tbl_extend('keep', format, default_status_signs) + result_str = vim + .iter(pairs(counts)) + :map(function(severity, count) + return ('%%#%s#%s:%s'):format(hl_map[severity], signs[severity], count) + end) + :join(' ') + elseif type(format) == 'function' then + result_str = format(counts) + end if result_str:len() > 0 then result_str = result_str .. '%##' end - return result_str end diff --git a/test/functional/lua/diagnostic_spec.lua b/test/functional/lua/diagnostic_spec.lua index 7ec6387ca4..7a93ffd705 100644 --- a/test/functional/lua/diagnostic_spec.lua +++ b/test/functional/lua/diagnostic_spec.lua @@ -4205,11 +4205,11 @@ describe('vim.diagnostic', function() ) end) - it('uses text from diagnostic.config().status.text[severity]', function() + it('uses text from diagnostic.config().status.format[severity]', function() local result = exec_lua(function() vim.diagnostic.config({ status = { - text = { + format = { [vim.diagnostic.severity.ERROR] = '⨯', [vim.diagnostic.severity.WARN] = '⚠︎', }, @@ -4226,6 +4226,45 @@ describe('vim.diagnostic', function() eq('%#DiagnosticSignError#⨯:1 %#DiagnosticSignWarn#⚠︎:1%##', result) end) + + it('uses format function diagnostic.config().status.format', function() + local result = exec_lua(function() + local signs = { + [vim.diagnostic.severity.ERROR] = 'EE', + [vim.diagnostic.severity.WARN] = 'WW', + [vim.diagnostic.severity.INFO] = 'II', + [vim.diagnostic.severity.HINT] = 'HH', + } + local hl_map = { + [vim.diagnostic.severity.ERROR] = 'ERROR', + [vim.diagnostic.severity.WARN] = 'WARN', + [vim.diagnostic.severity.INFO] = 'INFO', + [vim.diagnostic.severity.HINT] = 'HINT', + } + vim.diagnostic.config({ + status = { + format = function(counts) + local items = {} + for severity, sign in ipairs(signs) do + local count = counts[severity] or 0 + local hl = hl_map[severity] + table.insert(items, ('%%#%s#%s %s'):format(hl, sign, count)) + end + return table.concat(items, ' ') + end, + }, + }) + + vim.diagnostic.set(_G.diagnostic_ns, 0, { + _G.make_error('Error 1', 0, 1, 0, 1), + _G.make_warning('Warning 1', 2, 2, 2, 2), + }) + + return vim.diagnostic.status() + end) + + eq('%#ERROR#EE 1 %#WARN#WW 1 %#INFO#II 0 %#HINT#HH 0%##', result) + end) end) describe('handlers', function()