Excluding specific diagnostics in Neovim
Here's what I did to be able to filter out certain diagnostic messages in Neovim.
Note: I'm still learning (a) Lua, (b) the API surface of Neovim and (c) how the different core components interact and work together, so this may not be the best solution, but it works for me and I've learned a lot digging in and putting it together.
I recently revisited my Neovim config, with a view to updating and simplifying it with the advent of release 0.11. I covered some of this in my last Neovim related post A modern and clean Neovim setup for CAP Node.js - configuration and diagnostics. In that context, when editing Node.js (JavaScript) files, everything worked nicely with regards to the Language Server and how the diagnostics were surfaced.
A desire to filter out a specific diagnostic
That is, everything worked nicely ... with one annoyance - the hint level diagnostic "File is a CommonJS module" shown here on line 1:
H 1 const cds = require('@sap/cds') ■ File is a CommonJS module; it may be converted to an ES module.
2 module.exports = cds.service.impl(function() {
3 this.after('each', 'Books', book => {
4 console.log(book
E 5 }) ■ ',' expected.
6 })
For this sample display I'd temporarily changed my preferred config so that all diagnostics are shown, as virtual text, rather than virtual lines, like this:
:lua vim.diagnostic.config({virtual_lines = false, virtual_text = true})
So I wanted the language server to still publish diagnostics, but for me to be able to filter them.
Digging into the Neovim docu
In order to achieve this, I spent a pleasant morning today looking through the documentation and getting a better understanding of the API with respect to these features of Neovim (the links are to the documentation sections that are important here):
- Diagnostic framework
- LSP client / framework
- Lua engine (including the
vim
table-related functions)
The general idea
Neovim has mechanisms for firing up language servers, connecting to them and making their features and functionality available in buffers. It also has facilities for managing diagnostics and surfacing them in different ways.
With regards to diagnostics, a simplified flow between Neovim and a language server looks generally like this:
- Neovim attaches to a language server, sending the contents of the buffer to it for analysis
- The language server publishes hint, information, warning and error level diagnostics (via textDocument/publishDiagnostics)
- These diagnostics are stored in Neovim via a call to
vim.diagnostic.set
- Depending on the configuration, the diagnostics are displayed appropriately in the buffer
While I first went down the path of trying to add a filter in the last part (diagnostic display), I found that this was ultimately the wrong way to go about it, not least because I would have found myself having to override all the various diagnostic display affordances such as signs, virtual text, and so on.
The key was to interrupt the setting of the diagnostics so that I could filter some out before they were actually stored, which meant overriding vim.diagnostic.set
.
Examining the anatomy of a diagnostic
Understanding what a diagnostic looked like helped me enormously. In the JavaScript sample above, these two diagnostics are displayed:
- (H)INT 80001: File is a CommonJS module; it may be converted to an ES module.
- (E)RROR 1005: ',' expected
We can look at what these are via vim.diagnostic.get
. Invoking this:
:lua print(vim.diagnostic.get())
will just emit a table reference, something like this:
table: 0x68251cb7dca8
But we can use the vim.print
function instead:
:lua vim.print(vim.diagnostic.get())
or even simply the =
mechanism:
:lua =vim.diagnostic.get()
and this will cause a formatted display of the table contents:
{ {
bufnr = 1,
code = 1005,
col = 2,
end_col = 3,
end_lnum = 4,
lnum = 4,
message = "',' expected.",
namespace = 7,
severity = 1,
source = "typescript",
user_data = {
lsp = {
code = 1005,
message = "',' expected.",
range = {
["end"] = {
character = 3,
line = 4
},
start = {
character = 2,
line = 4
}
},
severity = 1,
source = "typescript",
tags = {}
}
}
}, {
bufnr = 1,
code = 80001,
col = 12,
end_col = 31,
end_lnum = 0,
lnum = 0,
message = "File is a CommonJS module; it may be converted to an ES module.",
namespace = 7,
severity = 4,
source = "typescript",
user_data = {
lsp = {
code = 80001,
message = "File is a CommonJS module; it may be converted to an ES module.",
range = {
["end"] = {
character = 31,
line = 0
},
start = {
character = 12,
line = 0
}
},
severity = 4,
source = "typescript",
tags = {}
}
}
} }
I decided that I would want to filter on code
and source
; the source
for both diagnostics here is typescript
, and the code
values are actually shown in the virtual text display already (80001
and 1005
).
How diagnostics are set
Diagnostics find their way from the language server back into Neovim via vim.diagnostic.set
which has this signature:
set({namespace}, {bufnr}, {diagnostics}, {opts})
I was curious as to what the namespace was; the value is shown as
7
for both diagnostic records above; looking at the namespaces with:lua =vim.api.nvim_get_namespaces()
shows this:{
lazy = 2,
["nvim.hlyank"] = 5,
["nvim.lsp.references"] = 3,
["nvim.lsp.semantic_tokens"] = 8,
["nvim.lsp.semantic_tokens:1"] = 9,
["nvim.lsp.signature_help"] = 6,
["nvim.terminal.prompt"] = 1,
["nvim.treesitter.highlighter"] = 4,
["nvim.vim.lsp.javascript.1.diagnostic.signs"] = 11,
["nvim.vim.lsp.javascript.1.diagnostic.underline"] = 12,
["nvim.vim.lsp.javascript.1.diagnostic.virtual_lines"] = 10,
["vim.lsp.javascript.1"] = 7
}which confirms that they're coming from the language server for TypeScript/JavaScript.
Injecting a filter into vim.diagnostic.set
Once I understood this, I was able to create a simple module (it's my first real foray into custom modules, so I may be doing this suboptimally), in ~/.config/nvim/lua/qmacro/diagnostic.lua
:
local M = {}
local original_vim_diagnostic_set = vim.diagnostic.set
local filterbuilder = function(filters)
return function(diagnostic)
for _, e in pairs(filters) do
if e.code == diagnostic.code and e.source == diagnostic.source then
-- if e.reason then
-- print('Filtering out', diagnostic.source, '/', diagnostic.code, 'diagnostic -', e.reason)
-- end
return false
end
end
return true
end
end
M.exclude = function(filters)
vim.diagnostic.set = function(ns, bufnr, diagnostics, opts)
local filtered_diagnostics = vim.tbl_filter(filterbuilder(filters), diagnostics)
original_vim_diagnostic_set(ns, bufnr, filtered_diagnostics, opts)
end
end
return M
Using the module
Before walking through this, I thought it would help to show how I want to call this, from within my ~/.config/nvim/init.lua
:
require('qmacro.diagnostic').exclude({
{ code = 80001, source = 'typescript', reason = 'yes I know already!' }
})
In calling the exclude
function in this module, I can pass a table of exclude filters, each of which has a code
and source
field, and an optional reason
field.
A walkthrough of the module
Now here's a brief breakdown of the module:
- I save the original
vim.diagnostic.set
function inoriginal_vim_diagnostic_set
so I can call it later - The
filterbuilder
function takes a table of filters and produces a function (yes, I like higher order functions), specifically a predicate function, that can be then used with vim.tbl_filter - The predicate function produced also has some commented-out logging (that uses the optional
reason
field from the filter entry) that I'll make good once I figure out the best way to log stuff cleanly and in accordance with standard levels
With all that preparation, all that I then have to do is:
- Override the standard
vim.diagnostic.set
function with one that injects a call tovim.tbl_filter
to remove any diagnostics that are caught by the exclude filters, before passing through the modified table and the rest of the original arguments to the originalvim.diagnostic.set
And that's pretty much it.
Wrapping up
This works for me so far, and getting to this stage has also taught me some more about Neovim's Lua API and various components.
I added this setup to my Neovim configuration with this commit to my dotfiles.
Along with the Neovim documentation itself, the following resources helped clarify things in my mind:
- Understanding Diagnostics in Neovim by Guillaume Humbert
- Filtering Neovim Diagnostics by Chakib Benziane
- Advent of Neovim by TJ DeVries
Thanks!