Skip to content

Commit 9047a35

Browse files
committed
feat: add lsp diagnostics and debounce tree refreshes, closes #7
1 parent 7b6d61c commit 9047a35

File tree

8 files changed

+202
-11
lines changed

8 files changed

+202
-11
lines changed

README.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,13 @@ function, either built-in or specified in your own the setup() config. Those
8181
functions are called with the config, node, and state of the plugin, and return
8282
the text and highlight group for the component.
8383

84-
Additionally, each source has a `before_render()` function that you can override
85-
and use to gather any additonal information you want to use in your components.
86-
This function is currently used to gather the git status for the tree. If you
87-
want to skip that, override the function and leave that part out. If you want
88-
to show LSP diagnostics, gather them in `before_render()`, create a component
89-
to display them, and reference that component in the renderer for the `file`
90-
and/or `directory` type.
84+
Additionally, each source has a `before_render()` function that you can
85+
override and use to gather any additonal information you want to use in your
86+
components. This function is currently used to gather the git status and
87+
diagnostics for the tree. If you want to skip that, override the function and
88+
leave that part out. If you want to show some other data, gather it in
89+
`before_render()`, create a component to display it, and reference that
90+
component in the renderer for the `file` and/or `directory` type.
9191

9292
Details on how to configure everything is in the help file at `:h neo-tree` or
9393
online at [neo-tree.txt](https://github.com/nvim-neo-tree/neo-tree.nvim/blob/main/doc/neo-tree.txt)

doc/neo-tree.txt

+21
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Mappings ............ |Neo-tree-Mappings|
1010
Filter ............ |Neo-tree-Filter|
1111
Configuration ....... |Neo-tree-Configuration|
1212
Setup ............. |Neo-tree-Setup|
13+
Diagnostics ....... |Neo-tree-Diagnostics|
1314
Highlights ........ |Neo-tree-Highlights|
1415
Other Sources ....... |Neo-tree-Sources|
1516
Buffers ........... |Neo-tree-Buffers|
@@ -253,6 +254,7 @@ require("neo-tree").setup({
253254
"clipboard",
254255
highlight = "NeoTreeDimText"
255256
},
257+
{ "diagnostics", errors_only = true },
256258
--{ "git_status" },
257259
},
258260
file = {
@@ -268,6 +270,7 @@ require("neo-tree").setup({
268270
"clipboard",
269271
highlight = "NeoTreeDimText"
270272
},
273+
{ "diagnostics" },
271274
{
272275
"git_status",
273276
highlight = "NeoTreeDimText"
@@ -278,6 +281,24 @@ require("neo-tree").setup({
278281
})
279282
<
280283

284+
DIAGNOSTICS *Neo-tree-Setup-Diagnostics*
285+
286+
By default, Neo-tree will display diagnostic symbols next to files. It will
287+
display the highest severity level for files, and errors only for directories.
288+
If you want to use symbols instead of "E", "W", "I", and H", you'll need to
289+
define those somewhere in your nvim configuration. Here is an example:
290+
291+
>
292+
vim.fn.sign_define("LspDiagnosticsSignError",
293+
{text = " ", texthl = "LspDiagnosticsSignError"})
294+
vim.fn.sign_define("LspDiagnosticsSignWarning",
295+
{text = " ", texthl = "LspDiagnosticsSignWarning"})
296+
vim.fn.sign_define("LspDiagnosticsSignInformation",
297+
{text = " ", texthl = "LspDiagnosticsSignInformation"})
298+
vim.fn.sign_define("LspDiagnosticsSignHint",
299+
{text = "", texthl = "LspDiagnosticsSignHint"})
300+
<
301+
281302
HIGHLIGHTS *Neo-tree-Highlights*
282303

283304
The following highlight groups are defined by this plugin. If you set any of

lua/neo-tree/sources/buffers/defaults.lua

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ local buffers = {
3131
-- data that can be used in the renderers.
3232
local utils = require("neo-tree.utils")
3333
state.git_status_lookup = utils.get_git_status()
34+
state.diagnostics_lookup = utils.get_diagnostic_counts()
3435
end,
3536
-- This section provides the renderers that will be used to render the tree.
3637
-- The first level is the node type.
@@ -47,6 +48,7 @@ local buffers = {
4748
padding = " ",
4849
},
4950
{ "name" },
51+
{ "diagnostics", errors_only = true },
5052
},
5153
file = {
5254
{
@@ -56,6 +58,7 @@ local buffers = {
5658
},
5759
{ "name" },
5860
{ "bufnr" },
61+
{ "diagnostics" },
5962
{ "git_status", highlight = highlights.DIM_TEXT },
6063
},
6164
}

lua/neo-tree/sources/buffers/init.lua

+13-2
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,19 @@ M.close = function()
2626
renderer.close(state)
2727
end
2828

29-
---Calld by autocmd when any buffer is open, closed, renamed, etc.
30-
M.buffers_changed = function()
29+
local buffers_changed_internal = function()
3130
for _, state in pairs(state_by_tab) do
3231
if state.path and renderer.window_exists(state) then
3332
items.get_open_buffers(state)
3433
end
3534
end
3635
end
3736

37+
---Calld by autocmd when any buffer is open, closed, renamed, etc.
38+
M.buffers_changed = function()
39+
utils.debounce("buffers_changed", buffers_changed_internal, 500)
40+
end
41+
3842
---Called by autocmds when the cwd dir is changed. This will change the root.
3943
M.dir_changed = function()
4044
local state = get_state()
@@ -115,6 +119,13 @@ M.setup = function(config)
115119
table.insert(autocmds, "autocmd BufFilePost * " .. refresh_cmd)
116120
table.insert(autocmds, "autocmd BufWritePost * " .. refresh_cmd)
117121
table.insert(autocmds, "autocmd BufDelete * " .. refresh_cmd)
122+
table.insert(autocmds, string.format([[
123+
if has('nvim-0.6')
124+
" Use the new diagnostic subsystem for neovim 0.6 and up
125+
au DiagnosticChanged * %s
126+
else
127+
au User LspDiagnosticsChanged * %s
128+
endif]], refresh_cmd, refresh_cmd))
118129
if default_config.bind_to_cwd then
119130
table.insert(autocmds, "autocmd DirChanged * :lua require('neo-tree.sources.buffers').dir_changed()")
120131
end

lua/neo-tree/sources/common/components.lua

+25
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,31 @@ M.current_filter = function(config, node, state)
4747
}
4848
end
4949

50+
M.diagnostics = function(config, node, state)
51+
local diag = state.diagnostics_lookup or {}
52+
local diag_state = diag[node:get_id()]
53+
if not diag_state then
54+
return {}
55+
end
56+
if config.errors_only and diag_state.severity_number > 1 then
57+
return {}
58+
end
59+
local severity = diag_state.severity_string
60+
local defined = vim.fn.sign_getdefined("LspDiagnosticsSign" .. severity)
61+
defined = defined and defined[1]
62+
if defined and defined.text and defined.texthl then
63+
return {
64+
text = " " .. defined.text,
65+
highlight = defined.texthl
66+
}
67+
else
68+
return {
69+
text = " " .. severity:sub(1, 1),
70+
highlight = "LspDiagnosticsDefault" .. severity
71+
}
72+
end
73+
end
74+
5075
M.git_status = function(config, node, state)
5176
local git_status_lookup = state.git_status_lookup
5277
if not git_status_lookup then

lua/neo-tree/sources/filesystem/defaults.lua

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ local filesystem = {
5555
-- data that can be used in the renderers.
5656
local utils = require("neo-tree.utils")
5757
state.git_status_lookup = utils.get_git_status()
58+
state.diagnostics_lookup = utils.get_diagnostic_counts()
5859
end,
5960
-- This section provides the renderers that will be used to render the tree.
6061
-- The first level is the node type.
@@ -76,6 +77,7 @@ local filesystem = {
7677
"clipboard",
7778
highlight = highlights.DIM_TEXT
7879
},
80+
{ "diagnostics", errors_only = true },
7981
--{ "git_status" },
8082
},
8183
file = {
@@ -90,6 +92,7 @@ local filesystem = {
9092
"clipboard",
9193
highlight = highlights.DIM_TEXT
9294
},
95+
{ "diagnostics" },
9396
{
9497
"git_status",
9598
highlight = highlights.DIM_TEXT

lua/neo-tree/sources/filesystem/init.lua

+17-2
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,13 @@ M.redraw = function()
247247
end
248248
end
249249

250+
---Refresh the tree, but not more often than frequency_in_ms
251+
---@param frequency_in_ms number The minimum time between refreshes.
252+
M.refresh_debounced = function(frequency_in_ms)
253+
frequency_in_ms = frequency_in_ms or 500
254+
utils.debounce("filesystem_refresh", M.refresh, frequency_in_ms)
255+
end
256+
250257
---Refreshes the tree by scanning the filesystem again.
251258
M.refresh = function()
252259
local state = get_state()
@@ -262,16 +269,24 @@ M.setup = function(config)
262269
if default_config == nil then
263270
default_config = config
264271
local autocmds = {}
265-
local refresh_cmd = ":lua require('neo-tree.sources.filesystem').refresh()"
272+
local refresh_cmd = ":lua require('neo-tree.sources.filesystem').refresh_debounced()"
266273
table.insert(autocmds, "augroup neotreefilesystem")
267274
table.insert(autocmds, "autocmd!")
268275
table.insert(autocmds, "autocmd BufWritePost * " .. refresh_cmd)
269276
table.insert(autocmds, "autocmd BufDelete * " .. refresh_cmd)
270277
if default_config.bind_to_cwd then
271278
table.insert(autocmds, "autocmd DirChanged * :lua require('neo-tree.sources.filesystem').dir_changed()")
272279
end
280+
table.insert(autocmds, string.format([[
281+
if has('nvim-0.6')
282+
" Use the new diagnostic subsystem for neovim 0.6 and up
283+
au DiagnosticChanged * %s
284+
else
285+
au User LspDiagnosticsChanged * %s
286+
endif]], refresh_cmd, refresh_cmd))
273287
table.insert(autocmds, "augroup END")
274-
vim.cmd(table.concat(autocmds, "\n"))
288+
local cmds = table.concat(autocmds, "\n")
289+
vim.cmd(cmds)
275290
end
276291
end
277292

lua/neo-tree/utils.lua

+113
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,119 @@ local function get_priority_git_status_code(status, other_status)
4545
end
4646
end
4747

48+
local diag_severity_to_string = function(severity)
49+
if severity == vim.diagnostic.severity.ERROR then
50+
return 'Error'
51+
elseif severity == vim.diagnostic.severity.WARN then
52+
return 'Warning'
53+
elseif severity == vim.diagnostic.severity.INFO then
54+
return 'Information'
55+
elseif severity == vim.diagnostic.severity.HINT then
56+
return 'Hint'
57+
else
58+
return nil
59+
end
60+
end
61+
62+
local tracked_functions = {}
63+
64+
65+
---Call fn, but not more than once every x milliseconds.
66+
---@param id string Identifier for the debounce group, such as the function name.
67+
---@param fn function Function to be executed.
68+
---@param frequency_in_ms number Miniumum amount of time between invocations of fn.
69+
---@param callback function Called with the result of executing fn as: callback(success, result)
70+
M.debounce = function(id, fn, frequency_in_ms, callback)
71+
local fn_data = tracked_functions[id]
72+
if fn_data == nil then
73+
-- first call for this id
74+
fn_data = {
75+
id = id,
76+
fn = nil,
77+
frequency_in_ms = frequency_in_ms,
78+
postponed_callback = nil,
79+
in_debounce_period = true,
80+
}
81+
tracked_functions[id] = fn_data
82+
else
83+
if fn_data.in_debounce_period then
84+
-- This id was called recently and can't be executed again yet.
85+
-- Just keep track of the details for this request so it
86+
-- can be executed at the end of the debounce period.
87+
-- Last one in wins.
88+
fn_data.fn = fn
89+
fn_data.frequency_in_ms = frequency_in_ms
90+
fn_data.postponed_callback = callback
91+
return
92+
end
93+
end
94+
95+
-- Run the requested function normally.
96+
-- Use a pcall to ensure the debounce period is still respected even if
97+
-- this call throws an error.
98+
fn_data.in_debounce_period = true
99+
local success, result = pcall(fn)
100+
101+
if not success then
102+
print("Error in neo-tree.utils.debounce: ", result)
103+
end
104+
105+
-- Now schedule the next earliest execution.
106+
-- If there are no calls to run the same function between now
107+
-- and when this deferred executes, nothing will happen.
108+
-- If there are several calls, only the last one in will run.
109+
vim.defer_fn(function ()
110+
local current_data = tracked_functions[id]
111+
local _callback = current_data.postponed_callback
112+
local _fn = current_data.fn
113+
current_data.postponed_callback = nil
114+
current_data.fn = nil
115+
current_data.in_debounce_period = false
116+
if _fn ~= nil then
117+
M.debounce(id, _fn, current_data.frequency_in_ms, _callback)
118+
end
119+
end, frequency_in_ms)
120+
121+
-- The callback function is outside the scope of the debounce period
122+
if type(callback) == "function" then
123+
callback(success, result)
124+
end
125+
end
126+
127+
128+
---Gets diagnostic severity counts for all files
129+
---@return table table { file_path = { Error = int, Warning = int, Information = int, Hint = int, Unknown = int } }
130+
M.get_diagnostic_counts = function ()
131+
local d = vim.diagnostic.get()
132+
local lookup = {}
133+
for _, diag in ipairs(d) do
134+
local file_name = vim.api.nvim_buf_get_name(diag.bufnr)
135+
local sev = diag_severity_to_string(diag.severity)
136+
if sev then
137+
local entry = lookup[file_name] or { severity_number = 4 }
138+
entry[sev] = (entry[sev] or 0) + 1
139+
entry.severity_number = math.min(entry.severity_number, diag.severity)
140+
entry.severity_string = diag_severity_to_string(entry.severity_number)
141+
lookup[file_name] = entry
142+
end
143+
end
144+
145+
for file_name, entry in pairs(lookup) do
146+
-- Now bubble this status up to the parent directories
147+
local parts = M.split(file_name, M.path_separator)
148+
table.remove(parts) -- pop the last part so we don't override the file's status
149+
M.reduce(parts, "", function (acc, part)
150+
local path = acc .. M.path_separator .. part
151+
local path_entry = lookup[path] or { severity_number = 4 }
152+
path_entry.severity_number = math.min(path_entry.severity_number, entry.severity_number)
153+
path_entry.severity_string = diag_severity_to_string(path_entry.severity_number)
154+
lookup[path] = path_entry
155+
return path
156+
end)
157+
end
158+
return lookup
159+
end
160+
48161
---Parse "git status" output for the current working directory.
49162
---@return table table Table with the path as key and the status as value.
50163
M.get_git_status = function ()

0 commit comments

Comments
 (0)