Meh I'll figure out submodules later

This commit is contained in:
mustard 2025-09-16 01:01:02 +02:00
parent 4ca9d44a90
commit 8cb281f436
352 changed files with 66107 additions and 0 deletions

View file

@ -0,0 +1,326 @@
local typecheck = require("neo-tree.health.typecheck")
local M = {}
local health = vim.health
local function check_dependencies()
local devicons_ok = pcall(require, "nvim-web-devicons")
if devicons_ok then
health.ok("nvim-web-devicons is installed")
else
health.info("nvim-web-devicons not installed")
end
local plenary_ok = pcall(require, "plenary")
if plenary_ok then
health.ok("plenary.nvim is installed")
else
health.error("plenary.nvim is not installed")
end
local nui_ok = pcall(require, "nui.tree")
if nui_ok then
health.ok("nui.nvim is installed")
else
health.error("nui.nvim not installed")
end
health.info("Optional dependencies for preview image support (only need one):")
-- optional
local snacks_ok = pcall(require, "snacks.image")
if snacks_ok then
health.ok("snacks.image is installed")
else
health.info("nui.nvim not installed")
end
local image_ok = pcall(require, "image")
if image_ok then
health.ok("image.nvim is installed")
else
health.info("nui.nvim not installed")
end
end
local validate = typecheck.validate
---@module "neo-tree.types.config"
---@param config neotree.Config.Base
function M.check_config(config)
---@type [string, string?][]
local errors = {}
local start = vim.uv.hrtime()
local verbose = vim.o.verbose > 0
local matched, missed = validate(
"config",
config,
function(cfg)
---@class neotree.health.Validator.Generators
local v = {
array = function(validator)
---@generic T
---@param arr T[]
return function(arr)
for i, val in ipairs(arr) do
validate(("[%d]"):format(i), val, validator)
end
end
end,
literal = function(literals)
return function(value)
return vim.tbl_contains(literals, value),
("value %s did not match literals %s"):format(value, table.concat(literals, "|"))
end
end,
}
local schema = {
Filesystem = {
---@param follow_current_file neotree.Config.Filesystem.FollowCurrentFile
FollowCurrentFile = function(follow_current_file)
validate("enabled", follow_current_file.enabled, "boolean", true)
validate("leave_dirs_open", follow_current_file.leave_dirs_open, "boolean", true)
end,
},
---@param window neotree.Config.Window
Window = function(window)
validate("mappings", window.mappings, "table") -- TODO: More specific validation for mappings table
end,
SourceSelector = {
---@param item neotree.Config.SourceSelector.Item
Item = function(item)
validate("source", item.source, "string")
validate("padding", item.padding, { "number", "table" }, true) -- TODO: More specific validation for padding table
validate("separator", item.separator, { "string", "table" }, true) -- TODO: More specific validation for separator table
end,
---@param sep neotree.Config.SourceSelector.Separator
Separator = function(sep)
validate("left", sep.left, "string")
validate("right", sep.right, "string")
validate("override", sep.override, v.literal({ "right", "left", "active" }), true)
end,
},
Renderers = v.array("table"),
}
if not validate("config", cfg, "table") then
health.error("Config does not exist")
return
end
validate("sources", cfg.sources, v.array("string"), false)
validate("add_blank_line_at_top", cfg.add_blank_line_at_top, "boolean")
validate("auto_clean_after_session_restore", cfg.auto_clean_after_session_restore, "boolean")
validate("close_if_last_window", cfg.close_if_last_window, "boolean")
validate("default_source", cfg.default_source, "string")
validate("enable_diagnostics", cfg.enable_diagnostics, "boolean")
validate("enable_git_status", cfg.enable_git_status, "boolean")
validate("enable_modified_markers", cfg.enable_modified_markers, "boolean")
validate("enable_opened_markers", cfg.enable_opened_markers, "boolean")
validate("enable_refresh_on_write", cfg.enable_refresh_on_write, "boolean")
validate("enable_cursor_hijack", cfg.enable_cursor_hijack, "boolean")
validate("git_status_async", cfg.git_status_async, "boolean")
validate("git_status_async_options", cfg.git_status_async_options, function(options)
validate("batch_size", options.batch_size, "number")
validate("batch_delay", options.batch_delay, "number")
validate("max_lines", options.max_lines, "number")
end)
validate("hide_root_node", cfg.hide_root_node, "boolean")
validate("retain_hidden_root_indent", cfg.retain_hidden_root_indent, "boolean")
validate(
"log_level",
cfg.log_level,
v.literal({ "trace", "debug", "info", "warn", "error", "fatal" }),
true
)
validate("log_to_file", cfg.log_to_file, { "boolean", "string" })
validate("open_files_in_last_window", cfg.open_files_in_last_window, "boolean")
validate(
"open_files_do_not_replace_types",
cfg.open_files_do_not_replace_types,
v.array("string")
)
validate("open_files_using_relative_paths", cfg.open_files_using_relative_paths, "boolean")
validate(
"popup_border_style",
cfg.popup_border_style,
v.literal({ "NC", "rounded", "single", "solid", "double", "" })
)
validate("resize_timer_interval", cfg.resize_timer_interval, "number")
validate("sort_case_insensitive", cfg.sort_case_insensitive, "boolean")
validate("sort_function", cfg.sort_function, "function", true)
validate("use_popups_for_input", cfg.use_popups_for_input, "boolean")
validate("use_default_mappings", cfg.use_default_mappings, "boolean")
validate("source_selector", cfg.source_selector, function(ss)
validate("winbar", ss.winbar, "boolean")
validate("statusline", ss.statusline, "boolean")
validate("show_scrolled_off_parent_node", ss.show_scrolled_off_parent_node, "boolean")
validate("sources", ss.sources, v.array(schema.SourceSelector.Item))
validate("content_layout", ss.content_layout, v.literal({ "start", "end", "center" }))
validate(
"tabs_layout",
ss.tabs_layout,
v.literal({ "equal", "start", "end", "center", "focus" })
)
validate("truncation_character", ss.truncation_character, "string", false)
validate("tabs_min_width", ss.tabs_min_width, "number", true)
validate("tabs_max_width", ss.tabs_max_width, "number", true)
validate("padding", ss.padding, { "number", "table" }) -- TODO: More specific validation for padding table
validate("separator", ss.separator, schema.SourceSelector.Separator)
validate("separator_active", ss.separator_active, schema.SourceSelector.Separator, true)
validate("show_separator_on_edge", ss.show_separator_on_edge, "boolean")
validate("highlight_tab", ss.highlight_tab, "string")
validate("highlight_tab_active", ss.highlight_tab_active, "string")
validate("highlight_background", ss.highlight_background, "string")
validate("highlight_separator", ss.highlight_separator, "string")
validate("highlight_separator_active", ss.highlight_separator_active, "string")
end)
validate("event_handlers", cfg.event_handlers, v.array("table"), true) -- TODO: More specific validation for event handlers
validate("default_component_configs", cfg.default_component_configs, function(defaults)
validate("container", defaults.container, "table") -- TODO: More specific validation
validate("indent", defaults.indent, "table") -- TODO: More specific validation
validate("icon", defaults.icon, "table") -- TODO: More specific validation
validate("modified", defaults.modified, "table") -- TODO: More specific validation
validate("name", defaults.name, "table") -- TODO: More specific validation
validate("git_status", defaults.git_status, "table") -- TODO: More specific validation
validate("file_size", defaults.file_size, "table") -- TODO: More specific validation
validate("type", defaults.type, "table") -- TODO: More specific validation
validate("last_modified", defaults.last_modified, "table") -- TODO: More specific validation
validate("created", defaults.created, "table") -- TODO: More specific validation
validate("symlink_target", defaults.symlink_target, "table") -- TODO: More specific validation
end)
validate("renderers", cfg.renderers, schema.Renderers)
validate("nesting_rules", cfg.nesting_rules, v.array("table"), true) -- TODO: More specific validation for nesting rules
validate("commands", cfg.commands, "table", true) -- TODO: More specific validation for commands
validate("window", cfg.window, function(window)
validate("position", window.position, "string") -- TODO: More specific validation
validate("width", window.width, "number")
validate("height", window.height, "number")
validate("auto_expand_width", window.auto_expand_width, "boolean")
validate("popup", window.popup, function(popup)
validate("title", popup.title, "function")
validate("size", popup.size, function(size)
validate("height", size.height, { "string", "number" })
validate("width", size.width, { "string", "number" })
end)
validate(
"border",
popup.border,
v.literal({ "NC", "rounded", "single", "solid", "double", "" }),
true
)
end)
validate("insert_as", window.insert_as, v.literal({ "child", "sibling" }), true)
validate("mapping_options", window.mapping_options, "table") -- TODO: More specific validation
validate("mappings", window.mappings, v.array("table")) -- TODO: More specific validation for mapping items
end)
validate("filesystem", cfg.filesystem, function(fs)
validate(
"async_directory_scan",
fs.async_directory_scan,
v.literal({ "auto", "always", "never" })
)
validate("scan_mode", fs.scan_mode, v.literal({ "shallow", "deep" }))
validate("bind_to_cwd", fs.bind_to_cwd, "boolean")
validate("cwd_target", fs.cwd_target, function(cwd_target)
validate("sidebar", cwd_target.sidebar, v.literal({ "tab", "window", "global" }))
validate("current", cwd_target.current, v.literal({ "tab", "window", "global" }))
end)
validate("check_gitignore_in_search", fs.check_gitignore_in_search, "boolean")
validate("filtered_items", fs.filtered_items, function(f)
validate("visible", f.visible, "boolean")
validate("force_visible_in_empty_folder", f.force_visible_in_empty_folder, "boolean")
validate("children_inherit_highlights", f.children_inherit_highlights, "boolean")
validate("show_hidden_count", f.show_hidden_count, "boolean")
validate("hide_dotfiles", f.hide_dotfiles, "boolean")
validate("hide_gitignored", f.hide_gitignored, "boolean")
validate("hide_hidden", f.hide_hidden, "boolean")
validate("hide_by_name", f.hide_by_name, v.array("string"))
validate("hide_by_pattern", f.hide_by_pattern, v.array("string"))
validate("always_show", f.always_show, v.array("string"))
validate("always_show_by_pattern", f.always_show_by_pattern, v.array("string"))
validate("never_show", f.never_show, v.array("string"))
validate("never_show_by_pattern", f.never_show_by_pattern, v.array("string"))
end)
validate("find_by_full_path_words", fs.find_by_full_path_words, "boolean")
validate("find_command", fs.find_command, "string", true)
validate("find_args", fs.find_args, { "table", "function" }, true)
validate("group_empty_dirs", fs.group_empty_dirs, "boolean")
validate("search_limit", fs.search_limit, "number")
validate("follow_current_file", fs.follow_current_file, schema.Filesystem.FollowCurrentFile)
validate(
"hijack_netrw_behavior",
fs.hijack_netrw_behavior,
v.literal({ "open_default", "open_current", "disabled" }),
true
)
validate("use_libuv_file_watcher", fs.use_libuv_file_watcher, "boolean")
validate("renderers", fs.renderers, schema.Renderers)
validate("window", fs.window, function(window)
validate("mappings", window.mappings, "table") -- TODO: More specific validation for mappings table
validate("fuzzy_finder_mappings", window.fuzzy_finder_mappings, "table") -- TODO: More specific validation
end)
end)
validate("buffers", cfg.buffers, function(buffers)
validate("bind_to_cwd", buffers.bind_to_cwd, "boolean")
validate(
"follow_current_file",
buffers.follow_current_file,
schema.Filesystem.FollowCurrentFile
)
validate("group_empty_dirs", buffers.group_empty_dirs, "boolean")
validate("show_unloaded", buffers.show_unloaded, "boolean")
validate("terminals_first", buffers.terminals_first, "boolean")
validate("renderers", buffers.renderers, schema.Renderers)
validate("window", buffers.window, schema.Window)
end)
validate("git_status", cfg.git_status, function(git_status)
validate("renderers", git_status.renderers, schema.Renderers)
validate("window", git_status.window, schema.Window)
end)
validate("document_symbols", cfg.document_symbols, function(ds)
validate("follow_cursor", ds.follow_cursor, "boolean")
validate("client_filters", ds.client_filters, { "string", "table" }) -- TODO: More specific validation
validate("custom_kinds", ds.custom_kinds, "table") -- TODO: More specific validation
validate("kinds", ds.kinds, "table")
validate("renderers", ds.renderers, schema.Renderers)
validate("window", ds.window, schema.Window)
end)
end,
false,
nil,
function(err)
errors[#errors + 1] = { err }
end,
true
)
local _end = vim.uv.hrtime()
if #errors == 0 then
health.ok("Configuration conforms to the neotree.Config.Base schema")
else
for _, err in ipairs(errors) do
health.error(unpack(err))
end
end
if verbose then
health.info(
"[verbose] Config schema checking is not comprehensive yet, unchecked keys listed below:"
)
if missed then
for _, miss in ipairs(missed) do
health.info(miss)
end
end
end
end
function M.check()
health.start("Dependencies")
check_dependencies()
health.start("Configuration")
local config = require("neo-tree").ensure_config()
M.check_config(config)
end
return M

View file

@ -0,0 +1,213 @@
local M = {}
---Like type() but also supports "callable" like neovim does.
---@see _G.type
---@param obj any
---@param expected neotree.LuaType
function M.match(obj, expected)
if type(obj) == expected then
return true
end
if expected == "callable" and vim.is_callable(obj) then
return true
end
return false
end
---@alias neotree.LuaType type|"callable"
---@alias neotree.health.ValidatorFunction<T> fun(value: T):boolean?,string?
---@alias neotree.health.Validator<T> elem_or_list<neotree.LuaType>|neotree.health.ValidatorFunction<T>
---@type (fun(err:string))[]
M.errfuncs = {}
---@type string[]
M.namestack = {}
---@generic T : table
---@param path string
---@param tbl T
---@param accesses string[]
---@param missed_paths table<string, true?>
---@return T mocked_tbl
local function mock_recursive(path, tbl, accesses, missed_paths, track_missed)
local mock_table = {}
---@class neotree.health.Mock.Metatable<T> : metatable
---@field accesses string[]
local mt = {
__original_table = tbl,
accesses = accesses,
}
---@return string[] missed_paths
mt.get_missed_paths = function()
---@type string[]
local missed_list = {}
if track_missed then
for p, _ in pairs(missed_paths) do
table.insert(missed_list, p)
end
end
table.sort(missed_list)
return missed_list
end
mt.__index = function(_, key)
local path_segment
if type(key) == "number" then
path_segment = ("[%02d]"):format(key)
else
path_segment = tostring(key)
end
local full_path
if path == "" then
full_path = path_segment
elseif type(key) == "number" then
full_path = path .. path_segment
else
full_path = path .. "." .. path_segment
end
-- Track accesses and missed accesses
mt.accesses[#mt.accesses + 1] = full_path
if track_missed then
missed_paths[full_path] = nil
end
local value = mt.__original_table[key]
if type(value) == "table" then
return mock_recursive(full_path, value, mt.accesses, missed_paths, track_missed)
end
return value
end
setmetatable(mock_table, mt)
return mock_table
end
--- Wraps a given table in a special mock table that tracks all accesses
--- (reads) to its fields and sub-fields. Optionally tracks unaccessed fields.
---
---@generic T : table
---@param name string The base name for the table, this forms the root of the access paths.
---@param tbl T The table to be mocked.
---@param track_missed boolean? Track which fields were NOT accessed.
---@return T mocked
function M.mock(name, tbl, track_missed)
local accesses = {}
local path_set = {}
track_missed = track_missed or false
if track_missed then
-- Generate another mock table and fully traverse that one first
local root_mock = M.mock(name, tbl, false)
---@param current_table table
local function deep_traverse_mock(current_table)
---@type neotree.health.Mock.Metatable
local mt = getmetatable(current_table)
for k, v in pairs(mt.__original_table) do
if type(v) == "table" then
deep_traverse_mock(current_table[k])
else
mt.__index(nil, k)
end
end
end
deep_traverse_mock(root_mock)
accesses = getmetatable(root_mock).accesses
for _, path in ipairs(accesses) do
path_set[path] = true
end
end
-- Start the recursive mocking process, passing all necessary shared tracking data.
return mock_recursive(name, tbl, accesses, path_set, track_missed)
end
---A comprehensive version of vim.validate that makes it easy to validate nested tables of various types
---@generic T
---@param name string
---@param value T
---@param validator neotree.health.Validator<T>
---@param optional? boolean Whether value can be nil
---@param message? string message when validation fails
---@param on_invalid? fun(err: string, value: T):boolean? What to do when a (nested) validation fails, return true to throw error
---@param track_missed? boolean Whether to return a second table that contains every non-checked field
---@return boolean valid
---@return string[]? missed
function M.validate(name, value, validator, optional, message, on_invalid, track_missed)
local matched, errmsg, errinfo
M.namestack[#M.namestack + 1] = name
if type(validator) == "string" then
matched = M.match(value, validator)
elseif type(validator) == "table" then
for _, v in ipairs(validator) do
matched = M.match(value, v)
if matched then
break
end
end
elseif type(validator) == "function" and value ~= nil then
local ok = false
if on_invalid then
M.errfuncs[#M.errfuncs + 1] = on_invalid
end
if track_missed and type(value) == "table" then
value = M.mock(name, value, true)
end
ok, matched, errinfo = pcall(validator, value)
if on_invalid then
M.errfuncs[#M.errfuncs] = nil
end
if not ok then
errinfo = matched
matched = false
elseif matched == nil then
matched = true
end
end
matched = matched or (optional and value == nil) or false
if not matched then
---@type string
local expected
if vim.is_callable(validator) then
expected = "?"
else
---@cast validator -function
local expected_types = type(validator) == "string" and { validator } or validator
---@cast expected_types -string
if optional then
expected_types[#expected_types + 1] = "nil"
end
expected = table.concat(expected_types, "|")
end
errmsg = ("%s: %s, got %s"):format(
table.concat(M.namestack, "."),
message or ("expected " .. expected),
message and value or type(value)
)
if errinfo then
errmsg = errmsg .. ", Info: " .. errinfo
end
local errfunc = M.errfuncs[#M.errfuncs]
local should_error = not errfunc or errfunc(errmsg)
if should_error then
M.namestack[#M.namestack] = nil
error(errmsg, 2)
end
end
M.namestack[#M.namestack] = nil
if track_missed then
local missed = getmetatable(value).get_missed_paths()
return matched, missed
end
return matched
end
return M