From b79ff967ac91dce40f3598ea407c5ccaa2929250 Mon Sep 17 00:00:00 2001 From: Anton Kastritskii Date: Wed, 30 Jul 2025 02:53:57 +0100 Subject: [PATCH] feat(statusline): vim.diagnostic.status() #33723 Problem: Not easy to get a status string for diagnostics. Solution: - Add vim.diagnostic.status(). - Add it to the default 'statusline'. --- runtime/doc/diagnostic.txt | 17 +++++++ runtime/doc/news.txt | 3 ++ runtime/doc/options.txt | 2 +- runtime/lua/vim/_meta/options.lua | 2 +- runtime/lua/vim/diagnostic.lua | 38 ++++++++++++++ src/nvim/options.lua | 22 +++++---- test/functional/lua/diagnostic_spec.lua | 66 +++++++++++++++++++++++++ test/functional/ui/statusline_spec.lua | 1 + test/old/testdir/test_options.vim | 6 ++- 9 files changed, 145 insertions(+), 12 deletions(-) diff --git a/runtime/doc/diagnostic.txt b/runtime/doc/diagnostic.txt index 28309b1454..dd5340a4d9 100644 --- a/runtime/doc/diagnostic.txt +++ b/runtime/doc/diagnostic.txt @@ -1019,6 +1019,23 @@ show({namespace}, {bufnr}, {diagnostics}, {opts}) • {opts} (`vim.diagnostic.Opts?`) Display options. See |vim.diagnostic.Opts|. +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 signs text with >lua + vim.diagnostic.config({ + signs = { text = { [vim.diagnostic.severity.ERROR] = 'e', ... } } + }) +< + + Parameters: ~ + • {bufnr} (`integer?`) Buffer number to get diagnostics from. Defaults + to 0 for the current buffer + + Return: ~ + (`string`) + toqflist({diagnostics}) *vim.diagnostic.toqflist()* Convert a list of diagnostics to a list of quickfix items that can be passed to |setqflist()| or |setloclist()|. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 109a92c920..75a0edb75f 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -145,6 +145,7 @@ DEFAULTS • 'statusline' default is exposed as a statusline expression (previously it was implemented as an internal C routine). +• 'statusline' includes |vim.diagnostic.status()| • Project-local configuration ('exrc') is also loaded from parent directories. Unset 'exrc' to stop further search. • Mappings: @@ -157,6 +158,8 @@ DIAGNOSTICS location/quickfix list. • |vim.diagnostic.get()| now accepts an `enabled` filter to only return enabled or disabled diagnostics. +• |vim.diagnostic.status()| returns a formatted string with current buffer + diagnostics EDITOR diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index a7820fda6c..4ca5db5eb6 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -6231,7 +6231,7 @@ A jump table for the options with a short description can be found at |Q_op|. an expensive expression can negatively affect render performance. *'statusline'* *'stl'* *E540* *E542* -'statusline' 'stl' string (default "%<%f %h%w%m%r %=%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}%{% &busy > 0 ? '◐ ' : '' %}%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}") +'statusline' 'stl' string (default is very long) global or local to window |global-local| Sets the |status-line|. diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index ee87ebc322..6d9ffb8850 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -6909,7 +6909,7 @@ vim.wo.stc = vim.wo.statuscolumn --- --- --- @type string -vim.o.statusline = "%<%f %h%w%m%r %=%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}%{% &busy > 0 ? '◐ ' : '' %}%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}" +vim.o.statusline = "%<%f %h%w%m%r %=%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}%{% &busy > 0 ? '◐ ' : '' %}%(%{luaeval('(pcall(require, ''vim.diagnostic'') and vim.diagnostic.status()) or '''' ')} %)%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}" vim.o.stl = vim.o.statusline vim.wo.statusline = vim.o.statusline vim.wo.stl = vim.wo.statusline diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 6ea6436c5b..1175ec6518 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -2840,4 +2840,42 @@ function M.fromqflist(list) return diagnostics end +--- 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 signs text with +--- ```lua +--- vim.diagnostic.config({ +--- signs = { text = { [vim.diagnostic.severity.ERROR] = 'e', ... } } +--- }) +--- ``` +---@param bufnr? integer Buffer number to get diagnostics from. +--- Defaults to 0 for the current buffer +--- +---@return string +function M.status(bufnr) + vim.validate('bufnr', bufnr, 'number', true) + bufnr = bufnr or 0 + local counts = M.count(bufnr) + local user_signs = vim.tbl_get(M.config() --[[@as vim.diagnostic.Opts]], 'signs', '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'):format(signs[severity], count) + end) + :join(' ') + + return result_str +end + +vim.api.nvim_create_autocmd('DiagnosticChanged', { + group = vim.api.nvim_create_augroup('nvim.diagnostic.status', {}), + callback = function(ev) + vim.api.nvim__redraw({ buf = ev.buf, statusline = true }) + end, + desc = 'diagnostics component for the statusline', +}) + return M diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 7bce4b49e9..12e403254d 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -8706,15 +8706,19 @@ local options = { { abbreviation = 'stl', cb = 'did_set_statusline', - defaults = table.concat({ - '%<', - '%f %h%w%m%r ', - '%=', - "%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}", - "%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}", - "%{% &busy > 0 ? '◐ ' : '' %}", - "%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}", - }), + defaults = { + if_true = table.concat({ + '%<', + '%f %h%w%m%r ', + '%=', + "%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}", + "%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}", + "%{% &busy > 0 ? '◐ ' : '' %}", + "%(%{luaeval('(pcall(require, ''vim.diagnostic'') and vim.diagnostic.status()) or '''' ')} %)", + "%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}", + }), + doc = 'is very long', + }, desc = [=[ Sets the |status-line|. diff --git a/test/functional/lua/diagnostic_spec.lua b/test/functional/lua/diagnostic_spec.lua index bff7da72ce..bf4cb02c4b 100644 --- a/test/functional/lua/diagnostic_spec.lua +++ b/test/functional/lua/diagnostic_spec.lua @@ -4022,6 +4022,72 @@ describe('vim.diagnostic', function() end) end) + describe('status()', function() + it('returns empty string if no diagnostics', function() + local result = exec_lua(function() + vim.diagnostic.set(_G.diagnostic_ns, _G.diagnostic_bufnr, {}) + return vim.diagnostic.status() + end) + + eq('', result) + end) + + it('returns count for each diagnostic kind', function() + local result = exec_lua(function() + 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), + _G.make_warning('Warning 2', 2, 2, 2, 2), + + _G.make_info('Info 1', 3, 3, 3, 3), + _G.make_info('Info 2', 3, 3, 3, 3), + _G.make_info('Info 3', 3, 3, 3, 3), + + _G.make_hint('Hint 1', 4, 4, 4, 4), + _G.make_hint('Hint 2', 4, 4, 4, 4), + _G.make_hint('Hint 3', 4, 4, 4, 4), + _G.make_hint('Hint 4', 4, 4, 4, 4), + }) + return vim.diagnostic.status() + end) + + eq('E:1 W:2 I:3 H:4', result) + + exec_lua('vim.cmd.enew()') + + -- Empty diagnostics for a buffer without diagnostics + eq( + '', + exec_lua(function() + return vim.diagnostic.status() + end) + ) + end) + + it('uses text from diagnostic.config().signs.text[severity]', function() + local result = exec_lua(function() + vim.diagnostic.config({ + signs = { + text = { + [vim.diagnostic.severity.ERROR] = '⨯', + [vim.diagnostic.severity.WARN] = '⚠︎', + }, + }, + }) + + 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('⨯:1 ⚠︎:1', result) + end) + end) + describe('handlers', function() it('checks that a new handler is a table', function() matches( diff --git a/test/functional/ui/statusline_spec.lua b/test/functional/ui/statusline_spec.lua index b7129dec24..059566abad 100644 --- a/test/functional/ui/statusline_spec.lua +++ b/test/functional/ui/statusline_spec.lua @@ -792,6 +792,7 @@ describe('default statusline', function() "%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}", "%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}", "%{% &busy > 0 ? '◐ ' : '' %}", + "%(%{luaeval('(pcall(require, ''vim.diagnostic'') and vim.diagnostic.status()) or '''' ')} %)", "%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}", }) diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim index d68af794e6..09776a4bf7 100644 --- a/test/old/testdir/test_options.vim +++ b/test/old/testdir/test_options.vim @@ -2690,7 +2690,7 @@ func GetGlobalLocalWindowOptions() " Filter for global or local to window v/^'.*'.*\n.*global or local to window |global-local/d " get option value and type - sil %s/^'\([^']*\)'.*'\s\+\(\w\+\)\s\+(default \%(\(".*"\|\d\+\|empty\)\).*/\1 \2 \3/g + sil %s/^'\([^']*\)'.*'\s\+\(\w\+\)\s\+(default \%(\(".*"\|\d\+\|empty\|is very long\)\).*/\1 \2 \3/g " sil %s/empty/""/g " split the result " let result=getline(1,'$')->map({_, val -> split(val, ' ')}) @@ -2705,6 +2705,10 @@ func Test_set_option_window_global_local_all() let optionlist = GetGlobalLocalWindowOptions() for [opt, type, default] in optionlist let _old = eval('&g:' .. opt) + if opt == 'statusline' + " parsed default value is "is very long" as it is a doc string, not actual value + let default = "\"" . _old . "\"" + endif if type == 'string' if opt == 'fillchars' exe 'setl ' .. opt .. '=vert:+'