Skip to content

Commit 299e174

Browse files
authored
feat: add healthcheck (#1773)
1 parent 940afae commit 299e174

File tree

1 file changed

+342
-0
lines changed

1 file changed

+342
-0
lines changed

lua/neo-tree/health/init.lua

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
local M = {}
2+
local health = vim.health
3+
4+
local function check_dependencies()
5+
local devicons_ok = pcall(require, "nvim-web-devicons")
6+
if devicons_ok then
7+
health.ok("nvim-web-devicons is installed")
8+
else
9+
health.info("nvim-web-devicons not installed")
10+
end
11+
12+
local plenary_ok = pcall(require, "plenary")
13+
if plenary_ok then
14+
health.ok("plenary.nvim is installed")
15+
else
16+
health.error("plenary.nvim is not installed")
17+
end
18+
19+
local nui_ok = pcall(require, "nui.tree")
20+
if nui_ok then
21+
health.ok("nui.nvim is installed")
22+
else
23+
health.error("nui.nvim not installed")
24+
end
25+
end
26+
27+
---@param config neotree.Config.Base
28+
function M.check_config(config)
29+
---@type [string, string][]
30+
local errors = {}
31+
32+
---@type string[]
33+
local index_path = {}
34+
local full_path = ""
35+
36+
---@generic T string
37+
---@param name string Argument name
38+
---@param value T Argument value
39+
---@param validator type|type[]|"callable"|fun(value: T):boolean?,string?
40+
---@param optional? boolean Argument is optional (may be omitted)
41+
---@param advice? string message when validation fails
42+
---@return boolean valid
43+
---@return string? errmsg
44+
local validate = function(name, value, validator, optional, advice)
45+
if type(validator) == "function" and type(value) == "table" then
46+
table.insert(index_path, name)
47+
full_path = table.concat(index_path, ".") .. "."
48+
local valid, errmsg = validator(value)
49+
table.remove(index_path)
50+
full_path = table.concat(index_path, ".") .. "."
51+
if valid == nil then
52+
valid = true
53+
end
54+
return valid, errmsg
55+
end
56+
57+
-- do regular validate
58+
local valid, errmsg = pcall(vim.validate, full_path .. name, value, validator, optional)
59+
if not valid then
60+
-- if type(validator) == "string" then
61+
-- advice = advice or ("Change this option to a %s"):format(validator)
62+
-- elseif type(validator) == "table" then
63+
-- advice = advice or ("Change this option to a %s"):format(table.concat(validator, "|"))
64+
-- end
65+
table.insert(errors, {
66+
errmsg,
67+
-- advice,
68+
})
69+
end
70+
return valid, errmsg
71+
end
72+
73+
---@class neotree.Validator.Generators
74+
---@field [string] fun(...):(fun(value: any):boolean,string?)
75+
local validator = {
76+
array = function(validator)
77+
---@generic T
78+
---@param arr T[]
79+
return function(arr)
80+
for i, val in ipairs(arr) do
81+
validate(("[%d]"):format(i), val, validator)
82+
end
83+
end
84+
end,
85+
literal = function(literals)
86+
return function(value)
87+
return vim.tbl_contains(literals, value),
88+
("value %s did not match literals %s"):format(value, table.concat(literals, "|"))
89+
end
90+
end,
91+
}
92+
local schema = {
93+
Filesystem = {
94+
---@param follow_current_file neotree.Config.Filesystem.FollowCurrentFile
95+
FollowCurrentFile = function(follow_current_file)
96+
validate("enabled", follow_current_file.enabled, "boolean", true)
97+
validate("leave_dirs_open", follow_current_file.leave_dirs_open, "boolean", true)
98+
end,
99+
},
100+
101+
Source = {
102+
---@param window neotree.Config.Source.Window
103+
Window = function(window)
104+
validate("mappings", window.mappings, "table") -- TODO: More specific validation for mappings table
105+
end,
106+
},
107+
SourceSelector = {
108+
---@param item neotree.Config.SourceSelector.Item
109+
Item = function(item)
110+
validate("source", item.source, "string")
111+
validate("padding", item.padding, { "number", "table" }, true) -- TODO: More specific validation for padding table
112+
validate("separator", item.separator, { "string", "table" }, true) -- TODO: More specific validation for separator table
113+
end,
114+
---@param sep neotree.Config.SourceSelector.Separator
115+
Separator = function(sep)
116+
validate("left", sep.left, "string")
117+
validate("right", sep.right, "string")
118+
validate(
119+
"override",
120+
sep.override,
121+
validator.literal({ "right", "left", "active", "nil" }),
122+
true
123+
)
124+
end,
125+
},
126+
Renderers = validator.array("table"),
127+
}
128+
129+
if not validate("config", config, "table", false) then
130+
health.error("Config does not exist")
131+
return
132+
end
133+
134+
validate("sources", config.sources, validator.array("string"), false)
135+
validate("add_blank_line_at_top", config.add_blank_line_at_top, "boolean")
136+
validate("auto_clean_after_session_restore", config.auto_clean_after_session_restore, "boolean")
137+
validate("close_if_last_window", config.close_if_last_window, "boolean")
138+
validate("default_source", config.default_source, "string")
139+
validate("enable_diagnostics", config.enable_diagnostics, "boolean")
140+
validate("enable_git_status", config.enable_git_status, "boolean")
141+
validate("enable_modified_markers", config.enable_modified_markers, "boolean")
142+
validate("enable_opened_markers", config.enable_opened_markers, "boolean")
143+
validate("enable_refresh_on_write", config.enable_refresh_on_write, "boolean")
144+
validate("enable_cursor_hijack", config.enable_cursor_hijack, "boolean")
145+
validate("git_status_async", config.git_status_async, "boolean")
146+
validate("git_status_async_options", config.git_status_async_options, function(options)
147+
validate("batch_size", options.batch_size, "number")
148+
validate("batch_delay", options.batch_delay, "number")
149+
validate("max_lines", options.max_lines, "number")
150+
end)
151+
validate("hide_root_node", config.hide_root_node, "boolean")
152+
validate("retain_hidden_root_indent", config.retain_hidden_root_indent, "boolean")
153+
validate(
154+
"log_level",
155+
config.log_level,
156+
validator.literal({ "trace", "debug", "info", "warn", "error", "fatal", "nil" })
157+
)
158+
validate("log_to_file", config.log_to_file, { "boolean", "string" })
159+
validate("open_files_in_last_window", config.open_files_in_last_window, "boolean")
160+
validate(
161+
"open_files_do_not_replace_types",
162+
config.open_files_do_not_replace_types,
163+
validator.array("string")
164+
)
165+
validate("open_files_using_relative_paths", config.open_files_using_relative_paths, "boolean")
166+
validate(
167+
"popup_border_style",
168+
config.popup_border_style,
169+
validator.literal({ "NC", "rounded", "single", "solid", "double", "" })
170+
)
171+
validate("resize_timer_interval", config.resize_timer_interval, "number")
172+
validate("sort_case_insensitive", config.sort_case_insensitive, "boolean")
173+
validate("sort_function", config.sort_function, "function", true)
174+
validate("use_popups_for_input", config.use_popups_for_input, "boolean")
175+
validate("use_default_mappings", config.use_default_mappings, "boolean")
176+
validate("source_selector", config.source_selector, function(ss)
177+
validate("winbar", ss.winbar, "boolean")
178+
validate("statusline", ss.statusline, "boolean")
179+
validate("show_scrolled_off_parent_node", ss.show_scrolled_off_parent_node, "boolean")
180+
validate("sources", ss.sources, validator.array(schema.SourceSelector.Item))
181+
validate("content_layout", ss.content_layout, validator.literal({ "start", "end", "center" }))
182+
validate(
183+
"tabs_layout",
184+
ss.tabs_layout,
185+
validator.literal({ "equal", "start", "end", "center", "focus" })
186+
)
187+
validate("truncation_character", ss.truncation_character, "string", false)
188+
validate("tabs_min_width", ss.tabs_min_width, "number", true)
189+
validate("tabs_max_width", ss.tabs_max_width, "number", true)
190+
validate("padding", ss.padding, { "number", "table" }) -- TODO: More specific validation for padding table
191+
validate("separator", ss.separator, schema.SourceSelector.Separator)
192+
validate("separator_active", ss.separator_active, schema.SourceSelector.Separator, true)
193+
validate("show_separator_on_edge", ss.show_separator_on_edge, "boolean")
194+
validate("highlight_tab", ss.highlight_tab, "string")
195+
validate("highlight_tab_active", ss.highlight_tab_active, "string")
196+
validate("highlight_background", ss.highlight_background, "string")
197+
validate("highlight_separator", ss.highlight_separator, "string")
198+
validate("highlight_separator_active", ss.highlight_separator_active, "string")
199+
end)
200+
validate("event_handlers", config.event_handlers, validator.array("table"), true) -- TODO: More specific validation for event handlers
201+
validate("default_component_configs", config.default_component_configs, function(defaults)
202+
validate("container", defaults.container, "table") -- TODO: More specific validation
203+
validate("indent", defaults.indent, "table") -- TODO: More specific validation
204+
validate("icon", defaults.icon, "table") -- TODO: More specific validation
205+
validate("modified", defaults.modified, "table") -- TODO: More specific validation
206+
validate("name", defaults.name, "table") -- TODO: More specific validation
207+
validate("git_status", defaults.git_status, "table") -- TODO: More specific validation
208+
validate("file_size", defaults.file_size, "table") -- TODO: More specific validation
209+
validate("type", defaults.type, "table") -- TODO: More specific validation
210+
validate("last_modified", defaults.last_modified, "table") -- TODO: More specific validation
211+
validate("created", defaults.created, "table") -- TODO: More specific validation
212+
validate("symlink_target", defaults.symlink_target, "table") -- TODO: More specific validation
213+
end)
214+
validate("renderers", config.renderers, schema.Renderers)
215+
validate("nesting_rules", config.nesting_rules, validator.array("table"), true) -- TODO: More specific validation for nesting rules
216+
validate("commands", config.commands, "table", true) -- TODO: More specific validation for commands
217+
validate("window", config.window, function(window)
218+
validate("position", window.position, "string") -- TODO: More specific validation
219+
validate("width", window.width, "number")
220+
validate("height", window.height, "number")
221+
validate("auto_expand_width", window.auto_expand_width, "boolean")
222+
validate("popup", window.popup, function(popup)
223+
validate("title", popup.title, "function")
224+
validate("size", popup.size, function(size)
225+
validate("height", size.height, { "string", "number" })
226+
validate("width", size.width, { "string", "number" })
227+
end)
228+
validate(
229+
"border",
230+
popup.border,
231+
validator.literal({ "NC", "rounded", "single", "solid", "double", "" }),
232+
true
233+
)
234+
end)
235+
validate("same_level", window.same_level, "boolean")
236+
validate("insert_as", window.insert_as, validator.literal({ "child", "sibling", "nil" }))
237+
validate("mapping_options", window.mapping_options, "table") -- TODO: More specific validation
238+
validate("mappings", window.mappings, validator.array("table")) -- TODO: More specific validation for mapping items
239+
end)
240+
241+
validate("filesystem", config.filesystem, function(fs)
242+
validate(
243+
"async_directory_scan",
244+
fs.async_directory_scan,
245+
validator.literal({ "auto", "always", "never" })
246+
)
247+
validate("scan_mode", fs.scan_mode, validator.literal({ "shallow", "deep" }))
248+
validate("bind_to_cwd", fs.bind_to_cwd, "boolean")
249+
validate("cwd_target", fs.cwd_target, function(cwd_target)
250+
validate("sidebar", cwd_target.sidebar, validator.literal({ "tab", "window", "global" }))
251+
validate("current", cwd_target.current, validator.literal({ "tab", "window", "global" }))
252+
end)
253+
validate("check_gitignore_in_search", fs.check_gitignore_in_search, "boolean")
254+
validate("filtered_items", fs.filtered_items, function(filtered_items)
255+
validate("visible", filtered_items.visible, "boolean")
256+
validate(
257+
"force_visible_in_empty_folder",
258+
filtered_items.force_visible_in_empty_folder,
259+
"boolean"
260+
)
261+
validate("show_hidden_count", filtered_items.show_hidden_count, "boolean")
262+
validate("hide_dotfiles", filtered_items.hide_dotfiles, "boolean")
263+
validate("hide_gitignored", filtered_items.hide_gitignored, "boolean")
264+
validate("hide_hidden", filtered_items.hide_hidden, "boolean")
265+
validate("hide_by_name", filtered_items.hide_by_name, validator.array("string"))
266+
validate("hide_by_pattern", filtered_items.hide_by_pattern, validator.array("string"))
267+
validate("always_show", filtered_items.always_show, validator.array("string"))
268+
validate(
269+
"always_show_by_pattern",
270+
filtered_items.always_show_by_pattern,
271+
validator.array("string")
272+
)
273+
validate("never_show", filtered_items.never_show, validator.array("string"))
274+
validate(
275+
"never_show_by_pattern",
276+
filtered_items.never_show_by_pattern,
277+
validator.array("string")
278+
)
279+
end)
280+
validate("find_by_full_path_words", fs.find_by_full_path_words, "boolean")
281+
validate("find_command", fs.find_command, "string", true)
282+
validate("find_args", fs.find_args, { "table", "function" }, true)
283+
validate("group_empty_dirs", fs.group_empty_dirs, "boolean")
284+
validate("search_limit", fs.search_limit, "number")
285+
validate("follow_current_file", fs.follow_current_file, schema.Filesystem.FollowCurrentFile)
286+
validate(
287+
"hijack_netrw_behavior",
288+
fs.hijack_netrw_behavior,
289+
validator.literal({ "open_default", "open_current", "disabled" }),
290+
true
291+
)
292+
validate("use_libuv_file_watcher", fs.use_libuv_file_watcher, "boolean")
293+
validate("renderers", fs.renderers, schema.Renderers)
294+
validate("window", fs.window, function(window)
295+
validate("mappings", window.mappings, "table") -- TODO: More specific validation for mappings table
296+
validate("fuzzy_finder_mappings", window.fuzzy_finder_mappings, "table") -- TODO: More specific validation
297+
end)
298+
end)
299+
validate("buffers", config.buffers, function(buffers)
300+
validate("bind_to_cwd", buffers.bind_to_cwd, "boolean")
301+
validate(
302+
"follow_current_file",
303+
buffers.follow_current_file,
304+
schema.Filesystem.FollowCurrentFile
305+
)
306+
validate("group_empty_dirs", buffers.group_empty_dirs, "boolean")
307+
validate("show_unloaded", buffers.show_unloaded, "boolean")
308+
validate("terminals_first", buffers.terminals_first, "boolean")
309+
validate("renderers", buffers.renderers, schema.Renderers)
310+
validate("window", buffers.window, schema.Source.Window)
311+
end)
312+
validate("git_status", config.git_status, function(git_status)
313+
validate("renderers", git_status.renderers, schema.Renderers)
314+
validate("window", git_status.window, schema.Source.Window)
315+
end)
316+
validate("document_symbols", config.document_symbols, function(document_symbols)
317+
validate("follow_cursor", document_symbols.follow_cursor, "boolean")
318+
validate("client_filters", document_symbols.client_filters, { "string", "table" }) -- TODO: More specific validation
319+
validate("custom_kinds", document_symbols.custom_kinds, "table") -- TODO: More specific validation
320+
validate("kinds", document_symbols.kinds, "table")
321+
validate("renderers", document_symbols.renderers, schema.Renderers)
322+
validate("window", document_symbols.window, schema.Source.Window)
323+
end)
324+
325+
if #errors == 0 then
326+
health.ok("Configuration conforms to schema")
327+
else
328+
for _, err in ipairs(errors) do
329+
health.error(unpack(err))
330+
end
331+
end
332+
end
333+
334+
function M.check()
335+
health.start("Neo-tree")
336+
check_dependencies()
337+
local config = require("neo-tree").ensure_config()
338+
M.check_config(config)
339+
health.info("(Config schema checking is not comprehensive yet)")
340+
end
341+
342+
return M

0 commit comments

Comments
 (0)