Skip to content
4 changes: 3 additions & 1 deletion lua/nvim-tree/actions/moves/item.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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
Expand Down
196 changes: 139 additions & 57 deletions lua/nvim-tree/diagnostics.lua
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

@alex-courtis alex-courtis Dec 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That one's a work in progress #2546

---@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<string, lsp.DiagnosticSeverity>

---@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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice name!

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 = {}

Expand All @@ -25,103 +51,159 @@ 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

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/ nit please require once at the start of the file

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
Expand Down
1 change: 1 addition & 0 deletions lua/nvim-tree/node.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions lua/nvim-tree/renderer/components/diagnostics.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local diagnostics = require "nvim-tree.diagnostics"

local M = {
HS_FILE = {},
Expand All @@ -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
Expand All @@ -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

Expand Down