diff --git a/lua/plugins/neotree.lua b/lua/plugins/neotree.lua new file mode 100644 index 0000000..6615e51 --- /dev/null +++ b/lua/plugins/neotree.lua @@ -0,0 +1,176 @@ +--- Neo-tree file explorer (replaces the mini.files explorer) +--- @module plugins.neotree +--- +--- This layers on top of AstroNvim's own neo-tree spec instead of replacing it: +--- `opts` takes the table AstroNvim built and only overrides what differs, so +--- `close_if_last_window`, `auto_clean_after_session_restore` (needed by the +--- resession dirsessions), `hijack_netrw_behavior` (`nvim `), the AstroUI +--- icon set and the `system_open`/`copy_selector`/`parent_or_close` commands +--- all stay in place. +--- +--- Filtering comes from two sources: +--- * static globs in `config.ignore_patterns` -> `hide_by_pattern` / +--- `never_show*` (neo-tree compiles globs to Lua patterns in `setup()`) +--- * neo-tree's own gitignore-style `ignore_files` reader for per-repo +--- `.rgignore` files, which replaces the old DirChanged / `set_root` +--- "pattern refresh" hooks. Those injected raw globs *after* setup() had +--- already compiled the list, so they silently matched nothing. + +local ignore_patterns = require "config.ignore_patterns" + +--- neo-tree rewrites these lists in place at `setup()` time (glob -> Lua +--- pattern), so hand it copies: the shared module keeps serving raw globs to +--- the snacks picker filter in astrocore.lua. +--- @param list string[] +--- @return string[] +local function copy(list) return vim.list_extend({}, list) end + +--- Open a hidden snacks terminal running `cmd`. +--- @param cmd string +--- @param opts? table extra snacks.terminal options +local function term(cmd, opts) + return function() + require("snacks").terminal(cmd, vim.tbl_extend("force", { hidden = true, interactive = true }, opts or {})) + end +end + +---@type LazySpec +return { + { + "nvim-neo-tree/neo-tree.nvim", + keys = { + -- kept from the mini.files era; AstroNvim's e is overridden by + -- the git files picker in astrocore.lua, o still focuses the tree + { "", "Neotree toggle", mode = { "n", "v" }, desc = "Toggle Explorer" }, + }, + opts = function(_, opts) + opts = require("astrocore").extend_tbl(opts, { + popup_border_style = "rounded", + enable_diagnostics = true, + git_status_async = true, -- asynchronous git status, improves performance + hide_root_node = true, + retain_hidden_root_indent = true, -- keep the indent so expanders line up + sort_case_insensitive = false, + source_selector = { winbar = false, statusline = false }, + window = { + position = "left", + width = 32, + -- window option, not a top level one (it used to be set top level, + -- where neo-tree ignored it) + auto_expand_width = true, + mappings = { + ["H"] = "navigate_up", + ["L"] = "set_root", + [""] = "toggle_hidden", + ["."] = "toggle_hidden", + ["/"] = "fuzzy_finder", + ["f"] = "filter_on_submit", + [""] = "clear_filter", + [""] = "open_and_close_neotree", + [""] = "open", + -- Preview image under cursor via kitty's icat kitten + ["p"] = "image_kitty", + -- Git keybindings, mirroring astrocore.lua + ["o"] = function() vim.cmd "!gh repo view --web" end, + ["h"] = term("gh dash", { auto_close = false }), + ["g"] = function() require("snacks").lazygit() end, + ["d"] = function() require("snacks").picker.git_diff() end, + ["b"] = function() require("snacks").picker.git_branches() end, + -- Terminals (toggleterm and config.terminals are both gone) + ["l"] = term("lazydocker", { auto_close = true, start_in_insert = true }), + ["."] = term("yazi", { auto_close = true, start_in_insert = true }), + ["t"] = function() require("snacks").terminal() end, + -- Open yazi in a detached kitty OS window at the node's directory + ["N"] = function(state) + local node = state.tree:get_node() + local path = node.type == "directory" and node.path or vim.fs.dirname(node.path) + vim.fn.jobstart { "kitty", "--detach", "--directory", path, "yazi" } + end, + }, + }, + default_component_configs = { + container = { + enable_character_fade = true, + }, + indent = { + indent_size = 2, + padding = 1, -- extra padding on left hand side + with_markers = true, + indent_marker = "│", + last_indent_marker = "└", + highlight = "NeoTreeIndentMarker", + expander_collapsed = "", + expander_expanded = "", + expander_highlight = "NeoTreeExpander", + }, + name = { + trailing_slash = false, + use_git_status_colors = true, + highlight = "NeoTreeFileName", + }, + git_status = { + symbols = { + -- change type: empty where the name already carries git colors + added = "", + modified = "", + deleted = "✖", + renamed = "󰁕", + -- status type + untracked = "", + ignored = "", + unstaged = "󰄱", + staged = "", + conflict = "", + }, + }, + -- extra columns, each only rendered once the window is wide enough + file_size = { enabled = true, width = 12, required_width = 48 }, + type = { enabled = true, width = 10, required_width = 122 }, + last_modified = { enabled = true, width = 20, required_width = 88 }, + created = { enabled = true, width = 20, required_width = 110 }, + symlink_target = { enabled = false }, + }, + filesystem = { + commands = { + -- Close neo-tree after opening a file (directories just expand) + open_and_close_neotree = function(state) + require("neo-tree.sources.filesystem.commands").open(state) + local tree = state.tree + local success, node = pcall(tree.get_node, tree) + if not success then return end + if node.type == "file" then require("neo-tree.command").execute { action = "close" } end + end, + -- Preview an image with kitty's icat kitten in a snacks terminal + image_kitty = function(state) + local node = state.tree:get_node() + if node and node.type == "file" then + term("kitten icat --hold " .. vim.fn.shellescape(node.path), { auto_close = false })() + else + vim.notify("Not a file", vim.log.levels.WARN, { title = "Neo-tree" }) + end + end, + }, + bind_to_cwd = true, + follow_current_file = { enabled = true, leave_dirs_open = false }, + use_libuv_file_watcher = true, -- filesystem option, not a top level one + filtered_items = { + force_visible_in_empty_folder = false, + show_hidden_count = false, + hide_dotfiles = false, + hide_gitignored = true, + -- gitignore-style ignore files, read per directory by neo-tree + hide_ignored = true, + ignore_files = { ".neotreeignore", ".ignore", ".rgignore" }, + hide_by_pattern = copy(ignore_patterns.patterns), + never_show = copy(ignore_patterns.never_show), + never_show_by_pattern = copy(ignore_patterns.never_show_patterns), + }, + }, + }) + -- deep merge of list values is index wise, so AstroNvim's extra sources + -- survive a `sources = { "filesystem" }` override: assign instead + opts.sources = { "filesystem" } + return opts + end, + }, +}