LSP Diagnostics, how and where to retrieve severity level to customise border color

Hi, new to neovim/LSP here. I was directed to the forums from issue 16627. I have been reading through the vim and lsp codebases, help pages, issues at github and threads in this forum. I wasn’t able to solve my “problem”. Please bear with my noob question.

I would like to know where is a good place to retrieve the severity level (I want to change the border color depending on severity level). My idea was to intercept it either in diagnostic.open_float or lsp.util.open_floating_preview but these are probably not the places for this. I was told in the issue, that I need to look at nvim_open_win but I don’t understand how that might help, since LSP diagnostics doesn’t call nvim_open_win directly (if I understood correctly), and it doesn’t have the severity info anyway.

My current code is:

-- Show line diagnostics in floating popup on hover, except insert mode (CursorHoldI)
vim.o.updatetime = 250
vim.cmd [[autocmd CursorHold * lua vim.diagnostic.open_float(nil, {focus=false})]]

-- Show source in diagnostics, not inline but as a floating popup
vim.diagnostic.config({
  virtual_text = false,
  float = {
    source = "always",  -- Or "if_many"
  },
})

-- create an array to hold custom border styles
local border = {
      {"🭽", "LspFloatWinBorder"},
      {"▔", "LspFloatWinBorder"},
      {"🭾", "LspFloatWinBorder"},
      {"▕", "LspFloatWinBorder"},
      {"🭿", "LspFloatWinBorder"},
      {"▁", "LspFloatWinBorder"},
      {"🭼", "LspFloatWinBorder"},
      {"▏", "LspFloatWinBorder"},
}

-- modify open_floating_preview to use the custom borders
local orig_util_open_floating_preview = vim.lsp.util.open_floating_preview
local open_floating_preview_custom = function(contents, syntax, opts, ...)
  opts = opts or {}
  -- Ideally I would like to retrieve severity here, to use that border array or another one. 
  opts.border = opts.border or border
  return orig_util_open_floating_preview(contents, syntax, opts, ...)
end
vim.lsp.util.open_floating_preview = open_floating_preview_custom

I think adding a plugin to only do that, as also suggested in the issue, is way overkill (besides, no plugin that I know of does this).

Thank you for your attention.
Sergi

A feature of some parts of the Nvim Lua API (including vim.diagnostic) is that you can override existing functions to modify how they execute. For instance:

vim.diagnostic.open_float = (function(orig)
  return function(opts)
    return orig(opts)
  end
end)(vim.diagnostic.open_float)

This example doesn’t do anything but call the original function again, so is a bit useless.

So you could do something like this:

vim.diagnostic.open_float = (function(orig)
    return function(opts)
        local lnum = vim.api.nvim_win_get_cursor(0)[1] - 1
        -- A more robust solution would check the "scope" value in `opts` to
        -- determine where to get diagnostics from, but if you're only using
        -- this for your own purposes you can make it as simple as you like
        local diagnostics = vim.diagnostic.get(opts.bufnr or 0, {lnum = lnum})
        local max_severity = vim.diagnostic.severity.HINT
        for _, d in ipairs(diagnostics) do
            -- Equality is "less than" based on how the severities are encoded
            if d.severity < max_severity then
                max_severity = d.severity
            end
        end
        local border_color = ({
            [vim.diagnostic.severity.HINT] = "NonText",
            [vim.diagnostic.severity.INFO] = "Question",
            [vim.diagnostic.severity.WARN] = "WarningMsg",
            [vim.diagnostic.severity.ERROR] = "ErrorMsg",
        })[max_severity]
        opts.border = {
            { "╔" , border_color },
            { "═" , border_color },
            { "╗" , border_color },
            { "║" , border_color },
            { "╝" , border_color },
            { "═" , border_color },
            { "╚" , border_color },
            { "║" , border_color },
        }
        orig(opts)
    end
end)(vim.diagnostic.open_float)
1 Like

Wow @gpanders thank you so much! I really tried hard, even to override vim.diagnostic.open_float (same as I did with vim.lsp.util.open_floating_preview) but as we said, diagnostics was not reachable there. I will wait for the PR to be merged and try this, but now I have a really nice hint to try by myself.

Also, thank you for showing me a more elegant way of overriding methods. :slight_smile:

You actually could do this now without the mentioned PR (I’ll update the accepted solution as well):

vim.diagnostic.open_float = (function(orig)
    return function(opts)
        local lnum = vim.api.nvim_win_get_cursor(0)[1] - 1
        -- A more robust solution would check the "scope" value in `opts` to
        -- determine where to get diagnostics from, but if you're only using
        -- this for your own purposes you can make it as simple as you like
        local diagnostics = vim.diagnostic.get(opts.bufnr or 0, {lnum = lnum})
        local max_severity = vim.diagnostic.severity.HINT
        for _, d in ipairs(diagnostics) do
            -- Equality is "less than" based on how the severities are encoded
            if d.severity < max_severity then
                max_severity = d.severity
            end
        end
        local border_color = ({
            [vim.diagnostic.severity.HINT] = "NonText",
            [vim.diagnostic.severity.INFO] = "Question",
            [vim.diagnostic.severity.WARN] = "WarningMsg",
            [vim.diagnostic.severity.ERROR] = "ErrorMsg",
        })[max_severity]
        opts.border = {
            { "╔" , border_color },
            { "═" , border_color },
            { "╗" , border_color },
            { "║" , border_color },
            { "╝" , border_color },
            { "═" , border_color },
            { "╚" , border_color },
            { "║" , border_color },
        }
        orig(opts)
    end
end)(vim.diagnostic.open_float)
1 Like

mmm, interesting. it throws an error though. i replaced my final chunk of code (from the border array to the end,) with yours, and get this:

Error detected while processing CursorHold Autocommands for "*":
E5108: Error executing lua ...gi/.config/nvim/lua/zigotica/plugins/lsp/diagnostics.lua:54: attempt to index local 'opts' (a nil value)
stack traceback:
        ...gi/.config/nvim/lua/zigotica/plugins/lsp/diagnostics.lua:54: in function 'open_float'
        [string ":lua"]:1: in main chunk

that line 54 refers to:

local diagnostics = vim.diagnostic.get(opts.bufnr or 0, {lnum = lnum})

same error if I move your code to just before the autocmd

OK I made it work, using an adaptation of your code. I just added bufnr as first param, following the help description for that function, AND a check for opts, AND also removed arguments from my autocmd. So my complete working code is:

-- wrap open_float to inspect diagnostics and use the severity color for border
-- https://neovim.discourse.group/t/lsp-diagnostics-how-and-where-to-retrieve-severity-level-to-customise-border-color/1679
vim.diagnostic.open_float = (function(orig)
    return function(bufnr, opts)
        local lnum = vim.api.nvim_win_get_cursor(0)[1] - 1
        local opts = opts or {}
        -- A more robust solution would check the "scope" value in `opts` to
        -- determine where to get diagnostics from, but if you're only using
        -- this for your own purposes you can make it as simple as you like
        local diagnostics = vim.diagnostic.get(opts.bufnr or 0, {lnum = lnum})
        local max_severity = vim.diagnostic.severity.HINT
        for _, d in ipairs(diagnostics) do
            -- Equality is "less than" based on how the severities are encoded
            if d.severity < max_severity then
                max_severity = d.severity
            end
        end
        local border_color = ({
            [vim.diagnostic.severity.HINT]  = "DiagnosticHint",
            [vim.diagnostic.severity.INFO]  = "DiagnosticInfo",
            [vim.diagnostic.severity.WARN]  = "DiagnosticWarn",
            [vim.diagnostic.severity.ERROR] = "DiagnosticError",
        })[max_severity]
        opts.border = {
            {"🭽", border_color},
            {"▔", border_color},
            {"🭾", border_color},
            {"▕", border_color},
            {"🭿", border_color},
            {"▁", border_color},
            {"🭼", border_color},
            {"▏", border_color},
        }
        orig(bufnr, opts)
    end
end)(vim.diagnostic.open_float)

-- Show line diagnostics in floating popup on hover, except insert mode (CursorHoldI)
vim.o.updatetime = 250
vim.cmd [[autocmd CursorHold * lua vim.diagnostic.open_float()]]

-- Show source in diagnostics, not inline but as a floating popup
vim.diagnostic.config({
  virtual_text = false,
  float = {
    source = "always",  -- Or "if_many"
  },
})

Thank you sooooo much again. I learnt a lot.

2 Likes

Ah yes, my example was based off of the latest master, which now accepts open_float without the bufnr argument. That change was introduced in the last ~week or so, so if you’re using 0.6 or a HEAD version older than that then you will still need the bufnr argument.

In any case, I’m glad you got it working!

1 Like

Hi! I know this concern’s already solved but do you know how to extract the window number (winnr)? I’m trying to remove the bg highlight in open_float but I don’t know where to get the winnr variable from the adaptation solution. Like this:

  local function custom_handler(handler)
    local overrides = { border = { "╔", "═", "╗", "║", "╝", "═", "╚", "║" } }
    return vim.lsp.with(function(...)
      local buf, winnr = handler(...)
      if buf then
        -- use the same transparency effect from cmp
        vim.api.nvim_win_set_option(winnr, 'winhighlight', 'Normal:CmpPmenu,FloatBorder:CmpPmenuBorder,CursorLine:PmenuSel,Search:None')
      end
    end, overrides)
  end

EDIT: Nevermind. Got it.