diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua index 9375ef5f4dc..bf72c0ea2df 100644 --- a/lua/nvim-tree/actions/moves/item.lua +++ b/lua/nvim-tree/actions/moves/item.lua @@ -3,6 +3,7 @@ local view = require "nvim-tree.view" local core = require "nvim-tree.core" local lib = require "nvim-tree.lib" local explorer_node = require "nvim-tree.explorer.node" +local diagnostics = require "nvim-tree.diagnostics" local M = {} @@ -33,7 +34,8 @@ function M.fn(opts) local git_status = explorer_node.get_git_status(node) valid = git_status ~= nil and (not opts.skip_gitignored or git_status[1] ~= "!!") elseif opts.what == "diag" then - valid = node.diag_status ~= nil + local diag_status = diagnostics.get_diag_status(node) + valid = diag_status ~= nil and diag_status.value ~= nil elseif opts.what == "opened" then valid = vim.fn.bufloaded(node.absolute_path) ~= 0 end diff --git a/lua/nvim-tree/diagnostics.lua b/lua/nvim-tree/diagnostics.lua index 93c689634a9..6ca13c9c067 100644 --- a/lua/nvim-tree/diagnostics.lua +++ b/lua/nvim-tree/diagnostics.lua @@ -1,18 +1,44 @@ local utils = require "nvim-tree.utils" local view = require "nvim-tree.view" -local core = require "nvim-tree.core" local log = require "nvim-tree.log" local M = {} -local severity_levels = { +---TODO add "$VIMRUNTIME" to "workspace.library" and use the @enum instead of this integer +---@alias lsp.DiagnosticSeverity integer + +---COC severity level strings to LSP severity levels +---@enum COC_SEVERITY_LEVELS +local COC_SEVERITY_LEVELS = { Error = 1, Warning = 2, Information = 3, Hint = 4, } ----@return table +---Absolute Node path to LSP severity level +---@alias NodeSeverities table + +---@class DiagStatus +---@field value lsp.DiagnosticSeverity|nil +---@field cache_version integer + +--- The buffer-severity mappings derived during the last diagnostic list update. +---@type NodeSeverities +local NODE_SEVERITIES = {} + +---The cache version number of the buffer-severity mappings. +---@type integer +local NODE_SEVERITIES_VERSION = 0 + +---@param path string +---@return string +local function uniformize_path(path) + return utils.canonical_path(path:gsub("\\", "/")) +end + +---Marshal severities from LSP. Does nothing when LSP disabled. +---@return NodeSeverities local function from_nvim_lsp() local buffer_severity = {} @@ -25,11 +51,10 @@ local function from_nvim_lsp() for _, diagnostic in ipairs(vim.diagnostic.get(nil, { severity = M.severity })) do local buf = diagnostic.bufnr if vim.api.nvim_buf_is_valid(buf) then - local bufname = vim.api.nvim_buf_get_name(buf) - local lowest_severity = buffer_severity[bufname] - if not lowest_severity or diagnostic.severity < lowest_severity then - buffer_severity[bufname] = diagnostic.severity - end + local bufname = uniformize_path(vim.api.nvim_buf_get_name(buf)) + local severity = diagnostic.severity + local highest_severity = buffer_severity[bufname] or severity + buffer_severity[bufname] = math.min(highest_severity, severity) end end end @@ -37,91 +62,148 @@ local function from_nvim_lsp() return buffer_severity end ----@param severity integer +---Severity is within diagnostics.severity.min, diagnostics.severity.max +---@param severity lsp.DiagnosticSeverity ---@param config table ---@return boolean local function is_severity_in_range(severity, config) return config.max <= severity and severity <= config.min end ----@return table -local function from_coc() - if vim.g.coc_service_initialized ~= 1 then - return {} +---Handle any COC exceptions, preventing any propagation +---@param err string +local function handle_coc_exception(err) + log.line("diagnostics", "handle_coc_exception: %s", vim.inspect(err)) + local notify = true + + -- avoid distractions on interrupts (CTRL-C) + if err:find "Vim:Interrupt" or err:find "Keyboard interrupt" then + notify = false end - local diagnostic_list = vim.fn.CocAction "diagnosticList" - if type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then - return {} + if notify then + require("nvim-tree.notify").error("Diagnostics update from coc.nvim failed. " .. vim.inspect(err)) end +end - local diagnostics = {} - for _, diagnostic in ipairs(diagnostic_list) do - local bufname = diagnostic.file - local coc_severity = severity_levels[diagnostic.severity] +---COC service initialized +---@return boolean +local function is_using_coc() + return vim.g.coc_service_initialized == 1 +end - local serverity = diagnostics[bufname] or vim.diagnostic.severity.HINT - diagnostics[bufname] = math.min(coc_severity, serverity) +---Marshal severities from COC. Does nothing when COC service not started. +---@return NodeSeverities +local function from_coc() + if not is_using_coc() then + return {} + end + + local ok, diagnostic_list = xpcall(function() + return vim.fn.CocAction "diagnosticList" + end, handle_coc_exception) + if not ok or type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then + return {} end local buffer_severity = {} - for bufname, severity in pairs(diagnostics) do - if is_severity_in_range(severity, M.severity) then - buffer_severity[bufname] = severity + for _, diagnostic in ipairs(diagnostic_list) do + local bufname = uniformize_path(diagnostic.file) + local coc_severity = COC_SEVERITY_LEVELS[diagnostic.severity] + local highest_severity = buffer_severity[bufname] or coc_severity + if is_severity_in_range(highest_severity, M.severity) then + buffer_severity[bufname] = math.min(highest_severity, coc_severity) end end return buffer_severity end -local function is_using_coc() - return vim.g.coc_service_initialized == 1 +---Maybe retrieve severity level from the cache +---@param node Node +---@return DiagStatus +local function from_cache(node) + local nodepath = uniformize_path(node.absolute_path) + local max_severity = nil + if not node.nodes then + -- direct cache hit for files + max_severity = NODE_SEVERITIES[nodepath] + else + -- dirs should be searched in the list of cached buffer names by prefix + for bufname, severity in pairs(NODE_SEVERITIES) do + local node_contains_buf = vim.startswith(bufname, nodepath .. "/") + if node_contains_buf then + if severity == M.severity.max then + max_severity = severity + break + else + max_severity = math.min(max_severity or severity, severity) + end + end + end + end + return { value = max_severity, cache_version = NODE_SEVERITIES_VERSION } end +---Fired on DiagnosticChanged and CocDiagnosticChanged events: +---debounced retrieval, cache update, version increment and draw function M.update() - if not M.enable or not core.get_explorer() or not view.is_buf_valid(view.get_bufnr()) then + if not M.enable then return end utils.debounce("diagnostics", M.debounce_delay, function() local profile = log.profile_start "diagnostics update" - log.line("diagnostics", "update") - - local buffer_severity if is_using_coc() then - buffer_severity = from_coc() + NODE_SEVERITIES = from_coc() else - buffer_severity = from_nvim_lsp() - end - - local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line()) - for _, node in pairs(nodes_by_line) do - node.diag_status = nil + NODE_SEVERITIES = from_nvim_lsp() end - - for bufname, severity in pairs(buffer_severity) do - local bufpath = utils.canonical_path(bufname) - log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity) - if 0 < severity and severity < 5 then - for line, node in pairs(nodes_by_line) do - local nodepath = utils.canonical_path(node.absolute_path) - log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath) - - local node_contains_buf = vim.startswith(bufpath:gsub("\\", "/"), nodepath:gsub("\\", "/") .. "/") - if M.show_on_dirs and node_contains_buf and (not node.open or M.show_on_open_dirs) then - log.line("diagnostics", " matched fold node '%s'", node.absolute_path) - node.diag_status = severity - elseif nodepath == bufpath then - log.line("diagnostics", " matched file node '%s'", node.absolute_path) - node.diag_status = severity - end - end + NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1 + if log.enabled "diagnostics" then + for bufname, severity in pairs(NODE_SEVERITIES) do + log.line("diagnostics", "Indexing bufname '%s' with severity %d", bufname, severity) end end log.profile_end(profile) - require("nvim-tree.renderer").draw() + if view.is_buf_valid(view.get_bufnr()) then + require("nvim-tree.renderer").draw() + end end) end +---Maybe retrieve diagnostic status for a node. +---Returns cached value when node's version matches. +---@param node Node +---@return DiagStatus|nil +function M.get_diag_status(node) + if not M.enable then + return nil + end + + -- dir but we shouldn't show on dirs at all + if node.nodes ~= nil and not M.show_on_dirs then + return nil + end + + -- here, we do a lazy update of the diagnostic status carried by the node. + -- This is by design, as diagnostics and nodes live in completely separate + -- worlds, and this module is the link between the two + if not node.diag_status or node.diag_status.cache_version < NODE_SEVERITIES_VERSION then + node.diag_status = from_cache(node) + end + + -- file + if not node.nodes then + return node.diag_status + end + + -- dir is closed or we should show on open_dirs + if not node.open or M.show_on_open_dirs then + return node.diag_status + end + return nil +end + function M.setup(opts) M.enable = opts.diagnostics.enable M.debounce_delay = opts.diagnostics.debounce_delay diff --git a/lua/nvim-tree/node.lua b/lua/nvim-tree/node.lua index 55ee1942dc4..323fc53c4c4 100644 --- a/lua/nvim-tree/node.lua +++ b/lua/nvim-tree/node.lua @@ -17,6 +17,7 @@ ---@field parent DirNode ---@field type string ---@field watcher function|nil +---@field diag_status DiagStatus|nil ---@class DirNode: BaseNode ---@field has_children boolean diff --git a/lua/nvim-tree/renderer/components/diagnostics.lua b/lua/nvim-tree/renderer/components/diagnostics.lua index 3a145406e70..1fa864deac9 100644 --- a/lua/nvim-tree/renderer/components/diagnostics.lua +++ b/lua/nvim-tree/renderer/components/diagnostics.lua @@ -1,4 +1,5 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION +local diagnostics = require "nvim-tree.diagnostics" local M = { HS_FILE = {}, @@ -17,10 +18,11 @@ function M.get_highlight(node) end local group + local diag_status = diagnostics.get_diag_status(node) if node.nodes then - group = M.HS_FOLDER[node.diag_status] + group = M.HS_FOLDER[diag_status and diag_status.value] else - group = M.HS_FILE[node.diag_status] + group = M.HS_FILE[diag_status and diag_status.value] end if group then @@ -35,7 +37,8 @@ end ---@return HighlightedString|nil modified icon function M.get_icon(node) if node and M.config.diagnostics.enable and M.config.renderer.icons.show.diagnostics then - return M.ICON[node.diag_status] + local diag_status = diagnostics.get_diag_status(node) + return M.ICON[diag_status and diag_status.value] end end