commit 01666aae64d15d88896f99bc064c7b2b4c2ed276
parent 0864939cc58d3f0af1a88a4baf34ed6b2a2c53ed
Author: Dmytro Pletenskyi <32138582+ph1losof@users.noreply.github.com>
Date: Sat, 14 Feb 2026 12:07:01 +0100
feat(diagnostic): fromqflist({merge_lines}) #37416
Problem:
`vim.diagnostic.fromqflist` ignores lines that are `item.valid == 0` (see
`getqflist`). Many qflists have messages that span multiple lines, which look
like this:
collection/src/Modelling/CdOd/Central.hs|496 col 80| error: [GHC-83865]
|| • Couldn't match expected type: InstanceWithForm
|| (FilePath
|| -> SelectValidCdInstWithForm
...
calling `vim.diagnostic.fromqflist(vim.fn.getqflist)` gets a diagnostic message
like this:
error: [GHC-83865]
only the first line is kept, but often, the remaing lines are useful as well.
Solution:
Introduce `merge_lines` option, which "squashes" lines from invalid qflist items
into the error message of the previous valid item, so that we get this
diagnostic message instead:
error: [GHC-83865]
• Couldn't match expected type: InstanceWithForm
(FilePath
-> SelectValidCdInstWithForm
Diffstat:
4 files changed, 121 insertions(+), 6 deletions(-)
diff --git a/runtime/doc/diagnostic.txt b/runtime/doc/diagnostic.txt
@@ -802,12 +802,16 @@ enable({enable}, {filter}) *vim.diagnostic.enable()*
• {bufnr}? (`integer`) Buffer number, or 0 for current
buffer, or `nil` for all buffers.
-fromqflist({list}) *vim.diagnostic.fromqflist()*
+fromqflist({list}, {opts}) *vim.diagnostic.fromqflist()*
Convert a list of quickfix items to a list of diagnostics.
Parameters: ~
- • {list} (`table[]`) List of quickfix items from |getqflist()| or
- |getloclist()|.
+ • {list} (`vim.quickfix.entry[]`) List of quickfix items from
+ |getqflist()| or |getloclist()|.
+ • {opts} (`table?`) Configuration table with the following keys:
+ • {merge_lines}? (`boolean`) When true, items with valid=0 are
+ appended to the previous valid item's message with a
+ newline. (default: false)
Return: ~
(`vim.Diagnostic[]`) See |vim.Diagnostic|.
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -198,6 +198,8 @@ DIAGNOSTICS
enabled or disabled diagnostics.
• |vim.diagnostic.status()| returns a formatted string with current buffer
diagnostics
+• |vim.diagnostic.fromqflist()| now accepts an `opts` table with
+ `merge_lines` to merge multi-line compiler messages.
EDITOR
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
@@ -2913,14 +2913,27 @@ function M.toqflist(diagnostics)
return list
end
+--- Configuration table with the following keys:
+--- @class vim.diagnostic.fromqflist.Opts
+--- @inlinedoc
+---
+--- When true, items with valid=0 are appended to the previous valid item's
+--- message with a newline. (default: false)
+--- @field merge_lines? boolean
+
--- Convert a list of quickfix items to a list of diagnostics.
---
----@param list table[] List of quickfix items from |getqflist()| or |getloclist()|.
+---@param list vim.quickfix.entry[] List of quickfix items from |getqflist()| or |getloclist()|.
+---@param opts? vim.diagnostic.fromqflist.Opts
---@return vim.Diagnostic[]
-function M.fromqflist(list)
+function M.fromqflist(list, opts)
vim.validate('list', list, 'table')
+ opts = opts or {}
+ local merge = opts.merge_lines
+
local diagnostics = {} --- @type vim.Diagnostic[]
+ local last_diag --- @type vim.Diagnostic?
for _, item in ipairs(list) do
if item.valid == 1 then
local lnum = math.max(0, item.lnum - 1)
@@ -2929,7 +2942,7 @@ function M.fromqflist(list)
local end_col = item.end_col > 0 and (item.end_col - 1) or col
local code = item.nr > 0 and item.nr or nil
local severity = item.type ~= '' and M.severity[item.type:upper()] or M.severity.ERROR
- diagnostics[#diagnostics + 1] = {
+ local diag = {
bufnr = item.bufnr,
lnum = lnum,
col = col,
@@ -2939,6 +2952,10 @@ function M.fromqflist(list)
message = item.text,
code = code,
}
+ diagnostics[#diagnostics + 1] = diag
+ last_diag = diag
+ elseif merge and last_diag then
+ last_diag.message = last_diag.message .. '\n' .. item.text
end
end
return diagnostics
diff --git a/test/functional/lua/diagnostic_spec.lua b/test/functional/lua/diagnostic_spec.lua
@@ -4089,6 +4089,98 @@ describe('vim.diagnostic', function()
end)
eq(result[1], result[2])
end)
+
+ it('merge_lines=true merges continuation lines', function()
+ local result = exec_lua(function()
+ local qflist = {
+ {
+ bufnr = 1,
+ lnum = 10,
+ col = 5,
+ end_lnum = 10,
+ end_col = 10,
+ text = 'error: [GHC-83865]',
+ type = 'E',
+ nr = 0,
+ valid = 1,
+ },
+ {
+ bufnr = 1,
+ lnum = 0,
+ col = 0,
+ end_lnum = 0,
+ end_col = 0,
+ text = " Couldn't match expected type",
+ type = '',
+ nr = 0,
+ valid = 0,
+ },
+ {
+ bufnr = 1,
+ lnum = 0,
+ col = 0,
+ end_lnum = 0,
+ end_col = 0,
+ text = ' with actual type',
+ type = '',
+ nr = 0,
+ valid = 0,
+ },
+ {
+ bufnr = 1,
+ lnum = 20,
+ col = 1,
+ end_lnum = 20,
+ end_col = 5,
+ text = 'warning: unused',
+ type = 'W',
+ nr = 0,
+ valid = 1,
+ },
+ }
+ return vim.diagnostic.fromqflist(qflist, { merge_lines = true })
+ end)
+
+ eq(2, #result)
+ eq(
+ "error: [GHC-83865]\n Couldn't match expected type\n with actual type",
+ result[1].message
+ )
+ eq('warning: unused', result[2].message)
+ end)
+
+ it('merge_lines=false ignores continuation lines', function()
+ local result = exec_lua(function()
+ local qflist = {
+ {
+ bufnr = 1,
+ lnum = 10,
+ col = 5,
+ end_lnum = 10,
+ end_col = 10,
+ text = 'error: main',
+ type = 'E',
+ nr = 0,
+ valid = 1,
+ },
+ {
+ bufnr = 1,
+ lnum = 0,
+ col = 0,
+ end_lnum = 0,
+ end_col = 0,
+ text = 'continuation',
+ type = '',
+ nr = 0,
+ valid = 0,
+ },
+ }
+ return vim.diagnostic.fromqflist(qflist)
+ end)
+
+ eq(1, #result)
+ eq('error: main', result[1].message)
+ end)
end)
describe('status()', function()