|
| 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