Guest User

diagnostics my current version

a guest
Dec 10th, 2025
49
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 5.43 KB | None | 0 0
  1. local M = {}
  2.  
  3. --- language-independent query for syntax errors and missing elements
  4. local ERROR_AND_MISSING = vim.treesitter.query.parse('query', '[(ERROR)(MISSING)] @_')
  5. --- query for no errors or missing
  6. local NONE = vim.treesitter.query.parse('query', '')
  7.  
  8. local config = {
  9.   parsers = {
  10.     -- parsers that are just problematic for this use-case
  11.     -- preprocessor causes tons of errors
  12.     c = NONE,
  13.     -- preprocessor causes tons of errors
  14.     cpp = NONE,
  15.     -- outdated, can't parse COPY --link or other modern syntax
  16.     dockerfile = NONE,
  17.     -- doesn't handle 'upstream' and other issues
  18.     nginx = NONE,
  19.     -- many errors/missing nodes
  20.     rust = NONE,
  21.     -- many error/missing nodes
  22.     groovy = NONE,
  23.     -- doesn't know specific dialects
  24.     sql = NONE,
  25.     -- many error nodes
  26.     helm = NONE,
  27.     -- many error nodes
  28.     vim = NONE,
  29.   },
  30. }
  31.  
  32. local namespace = vim.api.nvim_create_namespace('editor.treesitter.diagnostics')
  33.  
  34. --- @param parser vim.treesitter.LanguageTree
  35. --- @param query vim.treesitter.Query
  36. --- @param diagnostics vim.Diagnostic[]
  37. --- @param buffer integer
  38. local function diagnose_syntax(parser, query, diagnostics, buffer)
  39.   local root = parser:trees()[1]:root()
  40.   -- only process trees containing errors
  41.   if root:has_error() and query ~= NONE then
  42.     for _, match in query:iter_matches(root, buffer) do
  43.       for _, nodes in pairs(match) do
  44.         for _, node in ipairs(nodes) do
  45.           local lnum, col, end_lnum, end_col = node:range()
  46.  
  47.           -- collapse nested syntax errors that occur at the exact same position
  48.           local parent = node:parent()
  49.           if parent and parent:type() == 'ERROR' and parent:range() == node:range() then
  50.             goto continue
  51.           end
  52.  
  53.           -- clamp large syntax error ranges to just the line to reduce noise
  54.           if end_lnum > lnum then
  55.             end_lnum = lnum + 1
  56.             end_col = 0
  57.           end
  58.  
  59.           --- @type vim.Diagnostic
  60.           local diagnostic = {
  61.             severity = vim.diagnostic.severity.ERROR,
  62.             source = 'treesitter',
  63.             lnum = lnum,
  64.             end_lnum = end_lnum,
  65.             col = col,
  66.             end_col = end_col,
  67.             message = '',
  68.             bufnr = buffer,
  69.             namespace = namespace,
  70.           }
  71.  
  72.           if node:missing() then
  73.             diagnostic.severity = vim.diagnostic.severity.WARN
  74.             diagnostic.code = string.format('%s-missing', parser:lang())
  75.             diagnostic.message = string.format('missing `%s`', node:type())
  76.           else
  77.             diagnostic.severity = vim.diagnostic.severity.ERROR
  78.             diagnostic.code = string.format('%s-syntax', parser:lang())
  79.             diagnostic.message = 'error'
  80.           end
  81.  
  82.           -- add context to the error using sibling and parent nodes
  83.           local previous = node:prev_sibling()
  84.           if previous and previous:type() ~= 'ERROR' then
  85.             local previous_type = previous:named() and previous:type() or string.format('`%s`', previous:type())
  86.             diagnostic.message = diagnostic.message .. ' after ' .. previous_type
  87.           end
  88.  
  89.           if parent and parent:type() ~= 'ERROR' and (previous == nil or previous:type() ~= parent:type()) then
  90.             diagnostic.message = diagnostic.message .. ' in ' .. parent:type()
  91.           end
  92.  
  93.           table.insert(diagnostics, diagnostic)
  94.           ::continue::
  95.         end
  96.       end
  97.     end
  98.   end
  99. end
  100.  
  101. --- @param buffer integer
  102. local function diagnose_buffer(buffer)
  103.   if not vim.api.nvim_buf_is_valid(buffer) then
  104.     return
  105.   end
  106.   if not vim.diagnostic.is_enabled({ bufnr = buffer }) then
  107.     return
  108.   end
  109.  
  110.   --- @type vim.Diagnostic[]
  111.   local diagnostics = {}
  112.   local parser = vim.treesitter.get_parser(buffer, nil, { error = false })
  113.   if parser then
  114.     local query = vim.tbl_get(config, 'parsers', parser:lang()) or ERROR_AND_MISSING
  115.     if query and query ~= NONE then
  116.       parser:parse(false, function()
  117.         diagnose_syntax(parser, query, diagnostics, buffer)
  118.       end)
  119.       -- avoid updating in common case of no problems found and no problems found before
  120.       -- diagnostic updates can be a bit expensive
  121.       local update = #diagnostics > 0 or next(vim.diagnostic.count(buffer, { namespace = namespace }))
  122.       --- @diagnostic disable-next-line: unnecessary-if
  123.       if update then
  124.         vim.diagnostic.set(namespace, buffer, diagnostics)
  125.       end
  126.     end
  127.   end
  128. end
  129.  
  130. --- @param buffer integer
  131. function M.enable(buffer)
  132.   -- don't diagnose strange stuff
  133.   if vim.bo[buffer].buftype ~= '' then
  134.     return
  135.   end
  136.  
  137.   local timer = assert(vim.uv.new_timer())
  138.   local name = string.format('editor.syntax_%d', buffer)
  139.   local autocmd_group = vim.api.nvim_create_augroup(name, { clear = true })
  140.  
  141.   local run = vim.schedule_wrap(function()
  142.     diagnose_buffer(buffer)
  143.   end)
  144.  
  145.   -- lint now
  146.   run()
  147.  
  148.   -- lint on modifications
  149.   vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, {
  150.     desc = 'treesitter syntax',
  151.     buffer = buffer,
  152.     group = autocmd_group,
  153.     callback = function()
  154.       timer:stop()
  155.       timer:start(200, 0, run)
  156.     end,
  157.   })
  158.  
  159.   -- destroy resources
  160.   vim.api.nvim_create_autocmd({ 'BufUnload' }, {
  161.     desc = 'destroy linter',
  162.     buffer = buffer,
  163.     group = autocmd_group,
  164.     callback = function()
  165.       vim.api.nvim_del_augroup_by_id(autocmd_group)
  166.       timer:close()
  167.     end,
  168.   })
  169. end
  170.  
  171. return M
Advertisement
Add Comment
Please, Sign In to add comment