commit b79ff967ac91dce40f3598ea407c5ccaa2929250
parent 807a65b2da5390aa930ea11c12df2e60174420f1
Author: Anton Kastritskii <halloy52@gmail.com>
Date: Wed, 30 Jul 2025 02:53:57 +0100
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'.
Diffstat:
9 files changed, 145 insertions(+), 12 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:+'