From 610a1c189bdb2b9b936169b2ea9d1838f971fa2b Mon Sep 17 00:00:00 2001 From: Alexander Courtis Date: Sun, 3 Nov 2024 14:06:12 +1100 Subject: [PATCH] chore: resolve undefined-field warnings, fix link git statuses, rewrite devicons (#2968) * add todo * refactor(#2886): multi instance: node class refactoring: extract links, *_git_status (#2944) * extract DirectoryLinkNode and FileLinkNode, move Node methods to children * temporarily move DirectoryNode methods into BaseNode for easier reviewing * move mostly unchanged DirectoryNode methods back to BaseNode * tidy * git.git_status_file takes an array * update git status of links * luacheck hack * safer git_status_dir * refactor(#2886): multi instance: node class refactoring: DirectoryNode:expand_or_collapse (#2957) move expand_or_collapse to DirectoryNode * refactor(#2886): multi instance: node group functions refactoring (#2959) * move last_group_node to DirectoryNode * move add BaseNode:as and more doc * revert parameter name changes * revert parameter name changes * add Class * move group methods into DN * tidy group methods * tidy group methods * tidy group methods * tidy group methods * parent is DirectoryNode * tidy expand all * BaseNode -> Node * move watcher to DirectoryNode * last_group_node is DirectoryNode only * simplify create-file * simplify parent * simplify collapse-all * simplify live-filter * style * move lib.get_cursor_position to Explorer * move lib.get_node_at_cursor to Explorer * move lib.get_nodes to Explorer * move place_cursor_on_node to Explorer * resolve resource leak in purge_all_state * move many autocommands into Explorer * post merge tidy * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * Revert "chore: resolve undefined-field" This reverts commit be546ff18d41f28466b065c857e1e041659bd2c8. * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * Revert "chore: resolve undefined-field" This reverts commit e82db1c44d7dbe444737470af68d44239d78d429. * chore: resolve undefined-field * chore: class new is now generic * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * Revert "chore: resolve undefined-field" This reverts commit 0e9b844d22c737fa05eef2a72c2010d38a34ddec. * move icon builders into node classes * move icon builders into node classes * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * chore: resolve undefined-field * move folder specifics from icons to Directory * move folder specifics from icons to Directory * move folder specifics from icons to Directory * move folder specifics from icons to Directory * move file specifics from icons to File * clean up sorters * chore: resolve undefined-field * tidy hl icon name * file devicon uses library to fall back * file devicon uses library to fall back * file devicon uses library to fall back --- lua/nvim-tree.lua | 3 - lua/nvim-tree/actions/fs/clipboard.lua | 175 +++++++------- lua/nvim-tree/actions/fs/create-file.lua | 2 +- lua/nvim-tree/actions/fs/remove-file.lua | 5 +- lua/nvim-tree/actions/fs/rename-file.lua | 5 +- lua/nvim-tree/actions/fs/trash.lua | 5 +- lua/nvim-tree/actions/moves/item.lua | 44 ++-- lua/nvim-tree/api.lua | 23 +- lua/nvim-tree/buffers.lua | 27 ++- lua/nvim-tree/class.lua | 19 +- lua/nvim-tree/diagnostics.lua | 12 +- lua/nvim-tree/explorer/filters.lua | 28 +-- lua/nvim-tree/explorer/init.lua | 54 ++--- lua/nvim-tree/explorer/sorters.lua | 52 ++-- lua/nvim-tree/explorer/watch.lua | 10 +- lua/nvim-tree/git/init.lua | 226 +++++++++++------- lua/nvim-tree/git/runner.lua | 141 ++++++----- lua/nvim-tree/git/utils.lua | 86 +++++-- lua/nvim-tree/log.lua | 37 +-- lua/nvim-tree/marks/init.lua | 5 +- lua/nvim-tree/node/directory-link.lua | 50 +++- lua/nvim-tree/node/directory.lua | 138 ++++++++--- lua/nvim-tree/node/file-link.lua | 43 +++- lua/nvim-tree/node/file.lua | 69 +++++- lua/nvim-tree/node/init.lua | 97 +++----- lua/nvim-tree/node/root.lua | 2 +- lua/nvim-tree/renderer/builder.lua | 128 +--------- .../renderer/components/devicons.lua | 35 +++ .../renderer/components/diagnostics.lua | 4 +- lua/nvim-tree/renderer/components/icons.lua | 129 ---------- lua/nvim-tree/renderer/components/init.lua | 4 +- lua/nvim-tree/renderer/components/padding.lua | 9 +- .../renderer/decorator/bookmarks.lua | 2 +- lua/nvim-tree/renderer/decorator/copied.lua | 2 +- lua/nvim-tree/renderer/decorator/cut.lua | 2 +- .../renderer/decorator/diagnostics.lua | 5 +- lua/nvim-tree/renderer/decorator/git.lua | 70 +++--- lua/nvim-tree/renderer/decorator/hidden.lua | 6 +- lua/nvim-tree/renderer/decorator/modified.lua | 5 +- lua/nvim-tree/renderer/decorator/opened.lua | 2 +- lua/nvim-tree/renderer/init.lua | 5 - lua/nvim-tree/view.lua | 28 ++- lua/nvim-tree/watcher.lua | 176 ++++++++------ 43 files changed, 1078 insertions(+), 892 deletions(-) create mode 100644 lua/nvim-tree/renderer/components/devicons.lua delete mode 100644 lua/nvim-tree/renderer/components/icons.lua diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 4390adcf35b..0ece7f1916e 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -736,9 +736,6 @@ function M.setup(conf) require("nvim-tree.buffers").setup(opts) require("nvim-tree.help").setup(opts) require("nvim-tree.watcher").setup(opts) - if M.config.renderer.icons.show.file and pcall(require, "nvim-web-devicons") then - require("nvim-web-devicons").setup() - end setup_autocommands(opts) diff --git a/lua/nvim-tree/actions/fs/clipboard.lua b/lua/nvim-tree/actions/fs/clipboard.lua index 93c16e87606..1e786f025db 100644 --- a/lua/nvim-tree/actions/fs/clipboard.lua +++ b/lua/nvim-tree/actions/fs/clipboard.lua @@ -9,31 +9,34 @@ local find_file = require("nvim-tree.actions.finders.find-file").fn local DirectoryNode = require("nvim-tree.node.directory") ----@enum ACTION -local ACTION = { - copy = "copy", - cut = "cut", -} +---@alias ClipboardAction "copy" | "cut" +---@alias ClipboardData table + +---@alias ClipboardActionFn fun(source: string, dest: string): boolean, string? ---@class Clipboard to handle all actions.fs clipboard API ---@field config table hydrated user opts.filters ---@field private explorer Explorer ----@field private data table +---@field private data ClipboardData +---@field private clipboard_name string +---@field private reg string local Clipboard = {} ---@param opts table user options ---@param explorer Explorer ---@return Clipboard function Clipboard:new(opts, explorer) + ---@type Clipboard local o = { explorer = explorer, data = { - [ACTION.copy] = {}, - [ACTION.cut] = {}, + copy = {}, + cut = {}, }, + clipboard_name = opts.actions.use_system_clipboard and "system" or "neovim", + reg = opts.actions.use_system_clipboard and "+" or "1", config = { filesystem_watchers = opts.filesystem_watchers, - actions = opts.actions, }, } @@ -47,13 +50,11 @@ end ---@return boolean ---@return string|nil local function do_copy(source, destination) - local source_stats, handle - local success, errmsg + local source_stats, err = vim.loop.fs_stat(source) - source_stats, errmsg = vim.loop.fs_stat(source) if not source_stats then - log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, errmsg) - return false, errmsg + log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, err) + return false, err end log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination) @@ -64,25 +65,28 @@ local function do_copy(source, destination) end if source_stats.type == "file" then - success, errmsg = vim.loop.fs_copyfile(source, destination) + local success + success, err = vim.loop.fs_copyfile(source, destination) if not success then - log.line("copy_paste", "do_copy fs_copyfile failed '%s'", errmsg) - return false, errmsg + log.line("copy_paste", "do_copy fs_copyfile failed '%s'", err) + return false, err end return true elseif source_stats.type == "directory" then - handle, errmsg = vim.loop.fs_scandir(source) + local handle + handle, err = vim.loop.fs_scandir(source) if type(handle) == "string" then return false, handle elseif not handle then - log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, errmsg) - return false, errmsg + log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, err) + return false, err end - success, errmsg = vim.loop.fs_mkdir(destination, source_stats.mode) + local success + success, err = vim.loop.fs_mkdir(destination, source_stats.mode) if not success then - log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, errmsg) - return false, errmsg + log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, err) + return false, err end while true do @@ -93,15 +97,15 @@ local function do_copy(source, destination) local new_name = utils.path_join({ source, name }) local new_destination = utils.path_join({ destination, name }) - success, errmsg = do_copy(new_name, new_destination) + success, err = do_copy(new_name, new_destination) if not success then - return false, errmsg + return false, err end end else - errmsg = string.format("'%s' illegal file type '%s'", source, source_stats.type) - log.line("copy_paste", "do_copy %s", errmsg) - return false, errmsg + err = string.format("'%s' illegal file type '%s'", source, source_stats.type) + log.line("copy_paste", "do_copy %s", err) + return false, err end return true @@ -109,28 +113,26 @@ end ---@param source string ---@param dest string ----@param action ACTION ----@param action_fn fun(source: string, dest: string) +---@param action ClipboardAction +---@param action_fn ClipboardActionFn ---@return boolean|nil -- success ---@return string|nil -- error message local function do_single_paste(source, dest, action, action_fn) - local dest_stats - local success, errmsg, errcode local notify_source = notify.render_path(source) log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest) - dest_stats, errmsg, errcode = vim.loop.fs_stat(dest) - if not dest_stats and errcode ~= "ENOENT" then - notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (errmsg or "???")) - return false, errmsg + local dest_stats, err, err_name = vim.loop.fs_stat(dest) + if not dest_stats and err_name ~= "ENOENT" then + notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (err or "???")) + return false, err end local function on_process() - success, errmsg = action_fn(source, dest) + local success, error = action_fn(source, dest) if not success then - notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (errmsg or "???")) - return false, errmsg + notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (error or "???")) + return false, error end find_file(utils.path_remove_trailing(dest)) @@ -173,7 +175,7 @@ local function do_single_paste(source, dest, action, action_fn) end ---@param node Node ----@param clip table +---@param clip ClipboardData local function toggle(node, clip) if node.name == ".." then return @@ -191,8 +193,8 @@ end ---Clear copied and cut function Clipboard:clear_clipboard() - self.data[ACTION.copy] = {} - self.data[ACTION.cut] = {} + self.data.copy = {} + self.data.cut = {} notify.info("Clipboard has been emptied.") self.explorer.renderer:draw() end @@ -200,29 +202,32 @@ end ---Copy one node ---@param node Node function Clipboard:copy(node) - utils.array_remove(self.data[ACTION.cut], node) - toggle(node, self.data[ACTION.copy]) + utils.array_remove(self.data.cut, node) + toggle(node, self.data.copy) self.explorer.renderer:draw() end ---Cut one node ---@param node Node function Clipboard:cut(node) - utils.array_remove(self.data[ACTION.copy], node) - toggle(node, self.data[ACTION.cut]) + utils.array_remove(self.data.copy, node) + toggle(node, self.data.cut) self.explorer.renderer:draw() end ---Paste cut or cop ---@private ---@param node Node ----@param action ACTION ----@param action_fn fun(source: string, dest: string) +---@param action ClipboardAction +---@param action_fn ClipboardActionFn function Clipboard:do_paste(node, action, action_fn) if node.name == ".." then node = self.explorer - elseif node:is(DirectoryNode) then - node = node:last_group_node() + else + local dir = node:as(DirectoryNode) + if dir then + node = dir:last_group_node() + end end local clip = self.data[action] if #clip == 0 then @@ -230,10 +235,10 @@ function Clipboard:do_paste(node, action, action_fn) end local destination = node.absolute_path - local stats, errmsg, errcode = vim.loop.fs_stat(destination) - if not stats and errcode ~= "ENOENT" then - log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, errmsg) - notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (errmsg or "???")) + local stats, err, err_name = vim.loop.fs_stat(destination) + if not stats and err_name ~= "ENOENT" then + log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, err) + notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (err or "???")) return end local is_dir = stats and stats.type == "directory" @@ -278,24 +283,24 @@ end ---Paste cut (if present) or copy (if present) ---@param node Node function Clipboard:paste(node) - if self.data[ACTION.cut][1] ~= nil then - self:do_paste(node, ACTION.cut, do_cut) - elseif self.data[ACTION.copy][1] ~= nil then - self:do_paste(node, ACTION.copy, do_copy) + if self.data.cut[1] ~= nil then + self:do_paste(node, "cut", do_cut) + elseif self.data.copy[1] ~= nil then + self:do_paste(node, "copy", do_copy) end end function Clipboard:print_clipboard() local content = {} - if #self.data[ACTION.cut] > 0 then + if #self.data.cut > 0 then table.insert(content, "Cut") - for _, node in pairs(self.data[ACTION.cut]) do + for _, node in pairs(self.data.cut) do table.insert(content, " * " .. (notify.render_path(node.absolute_path))) end end - if #self.data[ACTION.copy] > 0 then + if #self.data.copy > 0 then table.insert(content, "Copy") - for _, node in pairs(self.data[ACTION.copy]) do + for _, node in pairs(self.data.copy) do table.insert(content, " * " .. (notify.render_path(node.absolute_path))) end end @@ -305,65 +310,45 @@ end ---@param content string function Clipboard:copy_to_reg(content) - local clipboard_name - local reg - if self.config.actions.use_system_clipboard == true then - clipboard_name = "system" - reg = "+" - else - clipboard_name = "neovim" - reg = "1" - end - -- manually firing TextYankPost does not set vim.v.event -- workaround: create a scratch buffer with the clipboard contents and send a yank command local temp_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_text(temp_buf, 0, 0, 0, 0, { content }) vim.api.nvim_buf_call(temp_buf, function() - vim.cmd(string.format('normal! "%sy$', reg)) + vim.cmd(string.format('normal! "%sy$', self.reg)) end) vim.api.nvim_buf_delete(temp_buf, {}) - notify.info(string.format("Copied %s to %s clipboard!", content, clipboard_name)) + notify.info(string.format("Copied %s to %s clipboard!", content, self.clipboard_name)) end ---@param node Node function Clipboard:copy_filename(node) - local content - if node.name == ".." then -- root - content = vim.fn.fnamemodify(self.explorer.absolute_path, ":t") + self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t")) else -- node - content = node.name + self:copy_to_reg(node.name) end - - self:copy_to_reg(content) end ---@param node Node function Clipboard:copy_basename(node) - local content - if node.name == ".." then -- root - content = vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r") + self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r")) else -- node - content = vim.fn.fnamemodify(node.name, ":r") + self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r")) end - - self:copy_to_reg(content) end ---@param node Node function Clipboard:copy_path(node) - local content - if node.name == ".." then -- root - content = utils.path_add_trailing("") + self:copy_to_reg(utils.path_add_trailing("")) else -- node local absolute_path = node.absolute_path @@ -373,10 +358,12 @@ function Clipboard:copy_path(node) end local relative_path = utils.path_relative(absolute_path, cwd) - content = node.nodes ~= nil and utils.path_add_trailing(relative_path) or relative_path + if node:is(DirectoryNode) then + self:copy_to_reg(utils.path_add_trailing(relative_path)) + else + self:copy_to_reg(relative_path) + end end - - self:copy_to_reg(content) end ---@param node Node @@ -394,14 +381,14 @@ end ---@param node Node ---@return boolean function Clipboard:is_cut(node) - return vim.tbl_contains(self.data[ACTION.cut], node) + return vim.tbl_contains(self.data.cut, node) end ---Node is copied. Will not be cut. ---@param node Node ---@return boolean function Clipboard:is_copied(node) - return vim.tbl_contains(self.data[ACTION.copy], node) + return vim.tbl_contains(self.data.copy, node) end return Clipboard diff --git a/lua/nvim-tree/actions/fs/create-file.lua b/lua/nvim-tree/actions/fs/create-file.lua index 455f6f564f7..86145cdb6d5 100644 --- a/lua/nvim-tree/actions/fs/create-file.lua +++ b/lua/nvim-tree/actions/fs/create-file.lua @@ -34,7 +34,7 @@ end ---@param node Node? function M.fn(node) - node = node or core.get_explorer() --[[@as Node]] + node = node or core.get_explorer() if not node then return end diff --git a/lua/nvim-tree/actions/fs/remove-file.lua b/lua/nvim-tree/actions/fs/remove-file.lua index 327a107122d..8a0f67cda3a 100644 --- a/lua/nvim-tree/actions/fs/remove-file.lua +++ b/lua/nvim-tree/actions/fs/remove-file.lua @@ -5,6 +5,9 @@ local view = require("nvim-tree.view") local lib = require("nvim-tree.lib") local notify = require("nvim-tree.notify") +local DirectoryLinkNode = require("nvim-tree.node.directory-link") +local DirectoryNode = require("nvim-tree.node.directory") + local M = { config = {}, } @@ -89,7 +92,7 @@ end ---@param node Node function M.remove(node) local notify_node = notify.render_path(node.absolute_path) - if node.nodes ~= nil and not node.link_to then + if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then local success = remove_dir(node.absolute_path) if not success then notify.error("Could not remove " .. notify_node) diff --git a/lua/nvim-tree/actions/fs/rename-file.lua b/lua/nvim-tree/actions/fs/rename-file.lua index efe40a34dd2..b4f9c945010 100644 --- a/lua/nvim-tree/actions/fs/rename-file.lua +++ b/lua/nvim-tree/actions/fs/rename-file.lua @@ -125,8 +125,9 @@ function M.fn(default_modifier) return end - if node:is(DirectoryNode) then - node = node:last_group_node() + local dir = node:as(DirectoryNode) + if dir then + node = dir:last_group_node() end if node.name == ".." then return diff --git a/lua/nvim-tree/actions/fs/trash.lua b/lua/nvim-tree/actions/fs/trash.lua index 15d96852a2b..c64dba0b72b 100644 --- a/lua/nvim-tree/actions/fs/trash.lua +++ b/lua/nvim-tree/actions/fs/trash.lua @@ -2,6 +2,9 @@ local core = require("nvim-tree.core") local lib = require("nvim-tree.lib") local notify = require("nvim-tree.notify") +local DirectoryLinkNode = require("nvim-tree.node.directory-link") +local DirectoryNode = require("nvim-tree.node.directory") + local M = { config = {}, } @@ -54,7 +57,7 @@ function M.remove(node) local explorer = core.get_explorer() - if node.nodes ~= nil and not node.link_to then + if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then trash_path(function(_, rc) if rc ~= 0 then notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash") diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua index 4390fa47564..8aacaae3b56 100644 --- a/lua/nvim-tree/actions/moves/item.lua +++ b/lua/nvim-tree/actions/moves/item.lua @@ -3,6 +3,7 @@ local view = require("nvim-tree.view") local core = require("nvim-tree.core") local diagnostics = require("nvim-tree.diagnostics") +local FileNode = require("nvim-tree.node.file") local DirectoryNode = require("nvim-tree.node.directory") local M = {} @@ -10,14 +11,14 @@ local MAX_DEPTH = 100 ---Return the status of the node or nil if no status, depending on the type of ---status. ----@param node table node to inspect ----@param what string type of status ----@param skip_gitignored boolean default false +---@param node Node to inspect +---@param what string? type of status +---@param skip_gitignored boolean? default false ---@return boolean local function status_is_valid(node, what, skip_gitignored) if what == "git" then - local git_status = node:get_git_status() - return git_status ~= nil and (not skip_gitignored or git_status[1] ~= "!!") + local git_xy = node:get_git_xy() + return git_xy ~= nil and (not skip_gitignored or git_xy[1] ~= "!!") elseif what == "diag" then local diag_status = diagnostics.get_diag_status(node) return diag_status ~= nil and diag_status.value ~= nil @@ -30,9 +31,9 @@ end ---Move to the next node that has a valid status. If none found, don't move. ---@param explorer Explorer ----@param where string where to move (forwards or backwards) ----@param what string type of status ----@param skip_gitignored boolean default false +---@param where string? where to move (forwards or backwards) +---@param what string? type of status +---@param skip_gitignored boolean? default false local function move(explorer, where, what, skip_gitignored) local first_node_line = core.get_nodes_starting_line() local nodes_by_line = utils.get_nodes_by_line(explorer.nodes, first_node_line) @@ -83,8 +84,8 @@ end --- Move to the next node recursively. ---@param explorer Explorer ----@param what string type of status ----@param skip_gitignored boolean default false +---@param what string? type of status +---@param skip_gitignored? boolean default false local function move_next_recursive(explorer, what, skip_gitignored) -- If the current node: -- * is a directory @@ -149,8 +150,8 @@ end --- 4.5) Save the current node and start back from 4.1. --- ---@param explorer Explorer ----@param what string type of status ----@param skip_gitignored boolean default false +---@param what string? type of status +---@param skip_gitignored boolean? default false local function move_prev_recursive(explorer, what, skip_gitignored) local node_init, node_cur @@ -175,7 +176,7 @@ local function move_prev_recursive(explorer, what, skip_gitignored) if node_cur == nil or node_cur == node_init -- we didn't move - or not node_cur.nodes -- node is a file + or node_cur:is(FileNode) -- node is a file then return end @@ -209,8 +210,10 @@ local function move_prev_recursive(explorer, what, skip_gitignored) end ---@class NavigationItemOpts ----@field where string ----@field what string +---@field where string? +---@field what string? +---@field skip_gitignored boolean? +---@field recurse boolean? ---@param opts NavigationItemOpts ---@return fun() @@ -222,26 +225,21 @@ function M.fn(opts) end local recurse = false - local skip_gitignored = false -- recurse only valid for git and diag moves. if (opts.what == "git" or opts.what == "diag") and opts.recurse ~= nil then recurse = opts.recurse end - if opts.skip_gitignored ~= nil then - skip_gitignored = opts.skip_gitignored - end - if not recurse then - move(explorer, opts.where, opts.what, skip_gitignored) + move(explorer, opts.where, opts.what, opts.skip_gitignored) return end if opts.where == "next" then - move_next_recursive(explorer, opts.what, skip_gitignored) + move_next_recursive(explorer, opts.what, opts.skip_gitignored) elseif opts.where == "prev" then - move_prev_recursive(explorer, opts.what, skip_gitignored) + move_prev_recursive(explorer, opts.what, opts.skip_gitignored) end end end diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua index a2796cb10a2..85762656a1d 100644 --- a/lua/nvim-tree/api.lua +++ b/lua/nvim-tree/api.lua @@ -9,6 +9,7 @@ local keymap = require("nvim-tree.keymap") local notify = require("nvim-tree.notify") local DirectoryNode = require("nvim-tree.node.directory") +local FileLinkNode = require("nvim-tree.node.file-link") local RootNode = require("nvim-tree.node.root") local Api = { @@ -140,8 +141,11 @@ end) Api.tree.change_root_to_node = wrap_node(function(node) if node.name == ".." or node:is(RootNode) then actions.root.change_dir.fn("..") - elseif node:is(DirectoryNode) then - actions.root.change_dir.fn(node:last_group_node().absolute_path) + else + node = node:as(DirectoryNode) + if node then + actions.root.change_dir.fn(node:last_group_node().absolute_path) + end end end) @@ -203,10 +207,8 @@ Api.fs.copy.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_pa ---@param mode string ---@param node Node local function edit(mode, node) - local path = node.absolute_path - if node.link_to and not node.nodes then - path = node.link_to - end + local file_link = node:as(FileLinkNode) + local path = file_link and file_link.link_to or node.absolute_path actions.node.open_file.fn(mode, path) end @@ -216,10 +218,13 @@ end local function open_or_expand_or_dir_up(mode, toggle_group) ---@param node Node return function(node) - if node.name == ".." then + local root = node:as(RootNode) + local dir = node:as(DirectoryNode) + + if root or node.name == ".." then actions.root.change_dir.fn("..") - elseif node:is(DirectoryNode) then - node:expand_or_collapse(toggle_group) + elseif dir then + dir:expand_or_collapse(toggle_group) elseif not toggle_group then edit(mode, node) end diff --git a/lua/nvim-tree/buffers.lua b/lua/nvim-tree/buffers.lua index 954c7e4011c..d53c1570df7 100644 --- a/lua/nvim-tree/buffers.lua +++ b/lua/nvim-tree/buffers.lua @@ -1,3 +1,5 @@ +local DirectoryNode = require("nvim-tree.node.directory") + local M = {} ---@type table record of which file is modified @@ -24,11 +26,26 @@ end ---@param node Node ---@return boolean function M.is_modified(node) - return node - and M.config.modified.enable - and M._modified[node.absolute_path] - and (not node.nodes or M.config.modified.show_on_dirs) - and (not node.open or M.config.modified.show_on_open_dirs) + if not M.config.modified.enable then + return false + end + + if not M._modified[node.absolute_path] then + return false + end + + local dir = node:as(DirectoryNode) + if dir then + if not M.config.modified.show_on_dirs then + return false + end + + if dir.open and not M.config.modified.show_on_open_dirs then + return false + end + end + + return true end ---A buffer exists for the node's absolute path diff --git a/lua/nvim-tree/class.lua b/lua/nvim-tree/class.lua index 8565e3c6ded..33b0b5bd33d 100644 --- a/lua/nvim-tree/class.lua +++ b/lua/nvim-tree/class.lua @@ -1,22 +1,24 @@ ---Generic class, useful for inheritence. ---@class (exact) Class ----@field private __index? table local Class = {} ----@param o Class? ----@return Class +---@generic T +---@param self T +---@param o T|nil +---@return T function Class:new(o) o = o or {} setmetatable(o, self) - self.__index = self + self.__index = self ---@diagnostic disable-line: inject-field return o end ---Object is an instance of class ---This will start with the lowest class and loop over all the superclasses. ----@param class table +---@generic T +---@param class T ---@return boolean function Class:is(class) local mt = getmetatable(self) @@ -32,9 +34,14 @@ end ---Return object if it is an instance of class, otherwise nil ---@generic T ---@param class T ----@return `T`|nil +---@return T|nil function Class:as(class) return self:is(class) and self or nil end +-- avoid unused param warnings in abstract methods +---@param ... any +function Class:nop(...) --luacheck: ignore 212 +end + return Class diff --git a/lua/nvim-tree/diagnostics.lua b/lua/nvim-tree/diagnostics.lua index 87d9edb26f5..3d004765343 100644 --- a/lua/nvim-tree/diagnostics.lua +++ b/lua/nvim-tree/diagnostics.lua @@ -3,6 +3,8 @@ local utils = require("nvim-tree.utils") local view = require("nvim-tree.view") local log = require("nvim-tree.log") +local DirectoryNode = require("nvim-tree.node.directory") + local M = {} ---COC severity level strings to LSP severity levels @@ -125,7 +127,7 @@ end local function from_cache(node) local nodepath = uniformize_path(node.absolute_path) local max_severity = nil - if not node.nodes then + if not node:is(DirectoryNode) then -- direct cache hit for files max_severity = NODE_SEVERITIES[nodepath] else @@ -190,7 +192,7 @@ function M.get_diag_status(node) end -- dir but we shouldn't show on dirs at all - if node.nodes ~= nil and not M.show_on_dirs then + if node:is(DirectoryNode) and not M.show_on_dirs then return nil end @@ -201,13 +203,15 @@ function M.get_diag_status(node) node.diag_status = from_cache(node) end + local dir = node:as(DirectoryNode) + -- file - if not node.nodes then + if not dir then return node.diag_status end -- dir is closed or we should show on open_dirs - if not node.open or M.show_on_open_dirs then + if not dir.open or M.show_on_open_dirs then return node.diag_status end return nil diff --git a/lua/nvim-tree/explorer/filters.lua b/lua/nvim-tree/explorer/filters.lua index 6cf21d4aad9..8de9f15076b 100644 --- a/lua/nvim-tree/explorer/filters.lua +++ b/lua/nvim-tree/explorer/filters.lua @@ -57,25 +57,25 @@ end ---Check if the given path is git clean/ignored ---@param path string Absolute path ----@param git_status table from prepare +---@param project GitProject from prepare ---@return boolean -local function git(self, path, git_status) - if type(git_status) ~= "table" or type(git_status.files) ~= "table" or type(git_status.dirs) ~= "table" then +local function git(self, path, project) + if type(project) ~= "table" or type(project.files) ~= "table" or type(project.dirs) ~= "table" then return false end -- default status to clean - local status = git_status.files[path] - status = status or git_status.dirs.direct[path] and git_status.dirs.direct[path][1] - status = status or git_status.dirs.indirect[path] and git_status.dirs.indirect[path][1] + local xy = project.files[path] + xy = xy or project.dirs.direct[path] and project.dirs.direct[path][1] + xy = xy or project.dirs.indirect[path] and project.dirs.indirect[path][1] -- filter ignored; overrides clean as they are effectively dirty - if self.config.filter_git_ignored and status == "!!" then + if self.config.filter_git_ignored and xy == "!!" then return true end -- filter clean - if self.config.filter_git_clean and not status then + if self.config.filter_git_clean and not xy then return true end @@ -178,14 +178,14 @@ local function custom(self, path) end ---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons. ----@param git_status table|nil optional results of git.load_project_status(...) +---@param project GitProject? optional results of git.load_projects(...) ---@return table ---- git_status: reference +--- project: reference --- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 } --- bookmarks: absolute paths to boolean -function Filters:prepare(git_status) +function Filters:prepare(project) local status = { - git_status = git_status or {}, + project = project or {}, bufinfo = {}, bookmarks = {}, } @@ -219,7 +219,7 @@ function Filters:should_filter(path, fs_stat, status) return false end - return git(self, path, status.git_status) + return git(self, path, status.project) or buf(self, path, status.bufinfo) or dotfile(self, path) or custom(self, path) @@ -240,7 +240,7 @@ function Filters:should_filter_as_reason(path, fs_stat, status) return FILTER_REASON.none end - if git(self, path, status.git_status) then + if git(self, path, status.project) then return FILTER_REASON.git elseif buf(self, path, status.bufinfo) then return FILTER_REASON.buf diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua index b60e57846af..ecb042d2dd3 100644 --- a/lua/nvim-tree/explorer/init.lua +++ b/lua/nvim-tree/explorer/init.lua @@ -59,7 +59,7 @@ function Explorer:create(path) local o = RootNode:create(explorer_placeholder, path, "..", nil) - o = self:new(o) --[[@as Explorer]] + o = self:new(o) o.explorer = o @@ -69,7 +69,7 @@ function Explorer:create(path) o.open = true o.opts = config - o.sorters = Sorters:new(config) + o.sorters = Sorters:create(config) o.renderer = Renderer:new(config, o) o.filters = Filters:new(config, o) o.live_filter = LiveFilter:new(config, o) @@ -187,9 +187,9 @@ function Explorer:expand(node) end ---@param node DirectoryNode ----@param git_status table|nil +---@param project GitProject? ---@return Node[]? -function Explorer:reload(node, git_status) +function Explorer:reload(node, project) local cwd = node.link_to or node.absolute_path local handle = vim.loop.fs_scandir(cwd) if not handle then @@ -198,7 +198,7 @@ function Explorer:reload(node, git_status) local profile = log.profile_start("reload %s", node.absolute_path) - local filter_status = self.filters:prepare(git_status) + local filter_status = self.filters:prepare(project) if node.group_next then node.nodes = { node.group_next } @@ -268,7 +268,7 @@ function Explorer:reload(node, git_status) end node.nodes = vim.tbl_map( - self:update_status(nodes_by_path, node_ignored, git_status), + self:update_git_statuses(nodes_by_path, node_ignored, project), vim.tbl_filter(function(n) if remain_childs[n.absolute_path] then return remain_childs[n.absolute_path] @@ -282,7 +282,7 @@ function Explorer:reload(node, git_status) local single_child = node:single_child_directory() if config.renderer.group_empty and node.parent and single_child then node.group_next = single_child - local ns = self:reload(single_child, git_status) + local ns = self:reload(single_child, project) node.nodes = ns or {} log.profile_end(profile) return ns @@ -321,7 +321,7 @@ function Explorer:refresh_parent_nodes_for_path(path) local project = git.get_project(toplevel) or {} self:reload(node, project) - node:update_parent_statuses(project, toplevel) + git.update_parent_projects(node, project, toplevel) end log.profile_end(profile) @@ -331,19 +331,19 @@ end ---@param node DirectoryNode function Explorer:_load(node) local cwd = node.link_to or node.absolute_path - local git_status = git.load_project_status(cwd) - self:explore(node, git_status, self) + local project = git.load_project(cwd) + self:explore(node, project, self) end ---@private ---@param nodes_by_path Node[] ---@param node_ignored boolean ----@param status table|nil ----@return fun(node: Node): table -function Explorer:update_status(nodes_by_path, node_ignored, status) +---@param project GitProject? +---@return fun(node: Node): Node +function Explorer:update_git_statuses(nodes_by_path, node_ignored, project) return function(node) if nodes_by_path[node.absolute_path] then - node:update_git_status(node_ignored, status) + node:update_git_status(node_ignored, project) end return node end @@ -353,13 +353,13 @@ end ---@param handle uv.uv_fs_t ---@param cwd string ---@param node DirectoryNode ----@param git_status table +---@param project GitProject ---@param parent Explorer -function Explorer:populate_children(handle, cwd, node, git_status, parent) +function Explorer:populate_children(handle, cwd, node, project, parent) local node_ignored = node:is_git_ignored() local nodes_by_path = utils.bool_record(node.nodes, "absolute_path") - local filter_status = parent.filters:prepare(git_status) + local filter_status = parent.filters:prepare(project) node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { git = 0, @@ -388,7 +388,7 @@ function Explorer:populate_children(handle, cwd, node, git_status, parent) if child then table.insert(node.nodes, child) nodes_by_path[child.absolute_path] = true - child:update_git_status(node_ignored, git_status) + child:update_git_status(node_ignored, project) end else for reason, value in pairs(FILTER_REASON) do @@ -405,10 +405,10 @@ end ---@private ---@param node DirectoryNode ----@param status table +---@param project GitProject ---@param parent Explorer ---@return Node[]|nil -function Explorer:explore(node, status, parent) +function Explorer:explore(node, project, parent) local cwd = node.link_to or node.absolute_path local handle = vim.loop.fs_scandir(cwd) if not handle then @@ -417,15 +417,15 @@ function Explorer:explore(node, status, parent) local profile = log.profile_start("explore %s", node.absolute_path) - self:populate_children(handle, cwd, node, status, parent) + self:populate_children(handle, cwd, node, project, parent) local is_root = not node.parent local single_child = node:single_child_directory() if config.renderer.group_empty and not is_root and single_child then local child_cwd = single_child.link_to or single_child.absolute_path - local child_status = git.load_project_status(child_cwd) + local child_project = git.load_project(child_cwd) node.group_next = single_child - local ns = self:explore(single_child, child_status, parent) + local ns = self:explore(single_child, child_project, parent) node.nodes = ns or {} log.profile_end(profile) @@ -440,7 +440,7 @@ function Explorer:explore(node, status, parent) end ---@private ----@param projects table +---@param projects GitProject[] function Explorer:refresh_nodes(projects) Iterator.builder({ self }) :applier(function(n) @@ -463,7 +463,7 @@ function Explorer:reload_explorer() end event_running = true - local projects = git.reload() + local projects = git.reload_all_projects() self:refresh_nodes(projects) if view.is_visible() then self.renderer:draw() @@ -477,8 +477,8 @@ function Explorer:reload_git() end event_running = true - local projects = git.reload() - self:reload_node_status(projects) + local projects = git.reload_all_projects() + git.reload_node_status(self, projects) self.renderer:draw() event_running = false end diff --git a/lua/nvim-tree/explorer/sorters.lua b/lua/nvim-tree/explorer/sorters.lua index f9e00166b1b..15ed921da05 100644 --- a/lua/nvim-tree/explorer/sorters.lua +++ b/lua/nvim-tree/explorer/sorters.lua @@ -1,16 +1,32 @@ -local C = {} - ----@class Sorter -local Sorter = {} +local Class = require("nvim-tree.class") +local DirectoryNode = require("nvim-tree.node.directory") -function Sorter:new(opts) - local o = {} - setmetatable(o, self) - self.__index = self - o.config = vim.deepcopy(opts.sort) +local C = {} - if type(o.config.sorter) == "function" then - o.user = o.config.sorter +---@class (exact) SorterCfg +---@field sorter string|fun(nodes: Node[]) +---@field folders_first boolean +---@field files_first boolean + +---@class (exact) Sorter: Class +---@field cfg SorterCfg +---@field user fun(nodes: Node[])? +---@field pre string? +local Sorter = Class:new() + +---@param opts table user options +---@return Sorter +function Sorter:create(opts) + ---@type Sorter + local o = { + cfg = vim.deepcopy(opts.sort), + } + o = self:new(o) + + if type(o.cfg.sorter) == "function" then + o.user = o.cfg.sorter --[[@as fun(nodes: Node[])]] + elseif type(o.cfg.sorter) == "string" then + o.pre = o.cfg.sorter --[[@as string]] end return o end @@ -20,7 +36,7 @@ end ---@return fun(a: Node, b: Node): boolean function Sorter:get_comparator(sorter) return function(a, b) - return (C[sorter] or C.name)(a, b, self.config) + return (C[sorter] or C.name)(a, b, self.cfg) end end @@ -41,17 +57,17 @@ end ---Evaluate `sort.folders_first` and `sort.files_first` ---@param a Node ---@param b Node ----@param cfg table +---@param cfg SorterCfg ---@return boolean|nil local function folders_or_files_first(a, b, cfg) if not (cfg.folders_first or cfg.files_first) then return end - if not a.nodes and b.nodes then + if not a:is(DirectoryNode) and b:is(DirectoryNode) then -- file <> folder return cfg.files_first - elseif a.nodes and not b.nodes then + elseif a:is(DirectoryNode) and not b:is(DirectoryNode) then -- folder <> file return not cfg.files_first end @@ -157,15 +173,15 @@ function Sorter:sort(t) end split_merge(t, 1, #t, mini_comparator) -- sort by user order - else - split_merge(t, 1, #t, self:get_comparator(self.config.sorter)) + elseif self.pre then + split_merge(t, 1, #t, self:get_comparator(self.pre)) end end ---@param a Node ---@param b Node ---@param ignorecase boolean|nil ----@param cfg table +---@param cfg SorterCfg ---@return boolean local function node_comparator_name_ignorecase_or_not(a, b, ignorecase, cfg) if not (a and b) then diff --git a/lua/nvim-tree/explorer/watch.lua b/lua/nvim-tree/explorer/watch.lua index a83758132ca..bd65ae53607 100644 --- a/lua/nvim-tree/explorer/watch.lua +++ b/lua/nvim-tree/explorer/watch.lua @@ -1,4 +1,5 @@ local log = require("nvim-tree.log") +local git = require("nvim-tree.git") local utils = require("nvim-tree.utils") local Watcher = require("nvim-tree.watcher").Watcher @@ -65,9 +66,10 @@ function M.create_watcher(node) return nil end + ---@param watcher Watcher local function callback(watcher) - log.line("watcher", "node event scheduled refresh %s", watcher.context) - utils.debounce(watcher.context, M.config.filesystem_watchers.debounce_delay, function() + log.line("watcher", "node event scheduled refresh %s", watcher.data.context) + utils.debounce(watcher.data.context, M.config.filesystem_watchers.debounce_delay, function() if watcher.destroyed then return end @@ -76,12 +78,12 @@ function M.create_watcher(node) else log.line("watcher", "node event executing refresh '%s'", node.absolute_path) end - node:refresh() + git.refresh_dir(node) end) end M.uid = M.uid + 1 - return Watcher:new(path, nil, callback, { + return Watcher:create(path, nil, callback, { context = "explorer:watch:" .. path .. ":" .. M.uid, }) end diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua index b16cc58320f..10354f85f30 100644 --- a/lua/nvim-tree/git/init.lua +++ b/lua/nvim-tree/git/init.lua @@ -1,24 +1,48 @@ local log = require("nvim-tree.log") local utils = require("nvim-tree.utils") local git_utils = require("nvim-tree.git.utils") -local Runner = require("nvim-tree.git.runner") + +local GitRunner = require("nvim-tree.git.runner") local Watcher = require("nvim-tree.watcher").Watcher local Iterator = require("nvim-tree.iterators.node-iterator") +local DirectoryNode = require("nvim-tree.node.directory") + +---Git short format status xy +---@alias GitXY string + +-- Git short-format status +---@alias GitPathXY table + +-- Git short-format statuses +---@alias GitPathXYs table + +---Git short-format statuses for a single node +---@class (exact) GitNodeStatus +---@field file GitXY? +---@field dir table<"direct" | "indirect", GitXY[]>? + +---Git state for an entire repo +---@class (exact) GitProject +---@field files GitProjectFiles? +---@field dirs GitProjectDirs? +---@field watcher Watcher? ----@class GitStatus ----@field file string|nil ----@field dir table|nil +---@alias GitProjectFiles GitPathXY +---@alias GitProjectDirs table<"direct" | "indirect", GitPathXYs> local M = { config = {}, - -- all projects keyed by toplevel + ---all projects keyed by toplevel + ---@type table _projects_by_toplevel = {}, - -- index of paths inside toplevels, false when not inside a project + ---index of paths inside toplevels, false when not inside a project + ---@type table _toplevels_by_path = {}, -- git dirs by toplevel + ---@type table _git_dirs_by_toplevel = {}, } @@ -34,35 +58,35 @@ local WATCHED_FILES = { ---@param toplevel string|nil ---@param path string|nil ----@param project table ----@param git_status table|nil -local function reload_git_status(toplevel, path, project, git_status) +---@param project GitProject +---@param project_files GitProjectFiles? +local function reload_git_project(toplevel, path, project, project_files) if path then for p in pairs(project.files) do if p:find(path, 1, true) == 1 then project.files[p] = nil end end - project.files = vim.tbl_deep_extend("force", project.files, git_status) + project.files = vim.tbl_deep_extend("force", project.files, project_files) else - project.files = git_status + project.files = project_files or {} end - project.dirs = git_utils.file_status_to_dir_status(project.files, toplevel) + project.dirs = git_utils.project_files_to_project_dirs(project.files, toplevel) end --- Is this path in a known ignored directory? ---@param path string ----@param project table git status +---@param project GitProject ---@return boolean local function path_ignored_in_project(path, project) if not path or not project then return false end - if project and project.files then - for file, status in pairs(project.files) do - if status == "!!" and vim.startswith(path, file) then + if project.files then + for p, xy in pairs(project.files) do + if xy == "!!" and vim.startswith(path, p) then return true end end @@ -70,9 +94,8 @@ local function path_ignored_in_project(path, project) return false end ---- Reload all projects ----@return table projects maybe empty -function M.reload() +---@return GitProject[] maybe empty +function M.reload_all_projects() if not M.config.git.enable then return {} end @@ -85,11 +108,12 @@ function M.reload() end --- Reload one project. Does nothing when no project or path is ignored ----@param toplevel string|nil ----@param path string|nil optional path to update only ----@param callback function|nil +---@param toplevel string? +---@param path string? optional path to update only +---@param callback function? function M.reload_project(toplevel, path, callback) - local project = M._projects_by_toplevel[toplevel] + local project = M._projects_by_toplevel[toplevel] --[[@as GitProject]] + if not toplevel or not project or not M.config.git.enable then if callback then callback() @@ -104,7 +128,8 @@ function M.reload_project(toplevel, path, callback) return end - local opts = { + ---@type GitRunnerOpts + local runner_opts = { toplevel = toplevel, path = path, list_untracked = git_utils.should_show_untracked(toplevel), @@ -113,20 +138,21 @@ function M.reload_project(toplevel, path, callback) } if callback then - Runner.run(opts, function(git_status) - reload_git_status(toplevel, path, project, git_status) + ---@param path_xy GitPathXY + runner_opts.callback = function(path_xy) + reload_git_project(toplevel, path, project, path_xy) callback() - end) + end + GitRunner:run(runner_opts) else -- TODO #1974 use callback once async/await is available - local git_status = Runner.run(opts) - reload_git_status(toplevel, path, project, git_status) + reload_git_project(toplevel, path, project, GitRunner:run(runner_opts)) end end --- Retrieve a known project ----@param toplevel string|nil ----@return table|nil project +---@param toplevel string? +---@return GitProject? project function M.get_project(toplevel) return M._projects_by_toplevel[toplevel] end @@ -147,11 +173,10 @@ function M.get_toplevel(path) return nil end - if M._toplevels_by_path[path] then - return M._toplevels_by_path[path] - end - - if M._toplevels_by_path[path] == false then + local tl = M._toplevels_by_path[path] + if tl then + return tl + elseif tl == false then return nil end @@ -190,8 +215,15 @@ function M.get_toplevel(path) end M._toplevels_by_path[path] = toplevel + M._git_dirs_by_toplevel[toplevel] = git_dir - return M._toplevels_by_path[path] + + toplevel = M._toplevels_by_path[path] + if toplevel == false then + return nil + else + return toplevel + end end local function reload_tree_at(toplevel) @@ -206,13 +238,13 @@ local function reload_tree_at(toplevel) end M.reload_project(toplevel, nil, function() - local git_status = M.get_project(toplevel) + local project = M.get_project(toplevel) Iterator.builder(root_node.nodes) :hidden() :applier(function(node) local parent_ignored = node.parent and node.parent:is_git_ignored() or false - node:update_git_status(parent_ignored, git_status) + node:update_git_status(parent_ignored, project) end) :recursor(function(node) return node.nodes and #node.nodes > 0 and node.nodes @@ -226,8 +258,8 @@ end --- Load the project status for a path. Does nothing when no toplevel for path. --- Only fetches project status when unknown, otherwise returns existing. ---@param path string absolute ----@return table project maybe empty -function M.load_project_status(path) +---@return GitProject maybe empty +function M.load_project(path) if not M.config.git.enable then return {} end @@ -238,12 +270,12 @@ function M.load_project_status(path) return {} end - local status = M._projects_by_toplevel[toplevel] - if status then - return status + local project = M._projects_by_toplevel[toplevel] + if project then + return project end - local git_status = Runner.run({ + local path_xys = GitRunner:run({ toplevel = toplevel, list_untracked = git_utils.should_show_untracked(toplevel), list_ignored = true, @@ -254,26 +286,27 @@ function M.load_project_status(path) if M.config.filesystem_watchers.enable then log.line("watcher", "git start") + ---@param w Watcher local callback = function(w) - log.line("watcher", "git event scheduled '%s'", w.toplevel) - utils.debounce("git:watcher:" .. w.toplevel, M.config.filesystem_watchers.debounce_delay, function() + log.line("watcher", "git event scheduled '%s'", w.data.toplevel) + utils.debounce("git:watcher:" .. w.data.toplevel, M.config.filesystem_watchers.debounce_delay, function() if w.destroyed then return end - reload_tree_at(w.toplevel) + reload_tree_at(w.data.toplevel) end) end local git_dir = vim.env.GIT_DIR or M._git_dirs_by_toplevel[toplevel] or utils.path_join({ toplevel, ".git" }) - watcher = Watcher:new(git_dir, WATCHED_FILES, callback, { + watcher = Watcher:create(git_dir, WATCHED_FILES, callback, { toplevel = toplevel, }) end - if git_status then + if path_xys then M._projects_by_toplevel[toplevel] = { - files = git_status, - dirs = git_utils.file_status_to_dir_status(git_status, toplevel), + files = path_xys, + dirs = git_utils.project_files_to_project_dirs(path_xys, toplevel), watcher = watcher, } return M._projects_by_toplevel[toplevel] @@ -283,46 +316,69 @@ function M.load_project_status(path) end end ----Git file and directory status for an absolute path with optional file fallback ----@param parent_ignored boolean ----@param status table|nil ----@param path string ----@param path_file string? alternative file path when no other file status ----@return GitStatus|nil -function M.git_status_dir(parent_ignored, status, path, path_file) - if parent_ignored then - return { file = "!!" } - end +---@param dir DirectoryNode +---@param project GitProject? +---@param root string? +function M.update_parent_projects(dir, project, root) + while project and dir do + -- step up to the containing project + if dir.absolute_path == root then + -- stop at the top of the tree + if not dir.parent then + break + end - if status then - return { - file = status.files and (status.files[path] or status.files[path_file]), - dir = status.dirs and { - direct = status.dirs.direct and status.dirs.direct[path], - indirect = status.dirs.indirect and status.dirs.indirect[path], - }, - } + root = M.get_toplevel(dir.parent.absolute_path) + + -- stop when no more projects + if not root then + break + end + + -- update the containing project + project = M.get_project(root) + M.reload_project(root, dir.absolute_path, nil) + end + + -- update status + dir:update_git_status(dir.parent and dir.parent:is_git_ignored() or false, project) + + -- maybe parent + dir = dir.parent end end ----Git file status for an absolute path with optional fallback ----@param parent_ignored boolean ----@param status table|nil ----@param path string ----@param path_fallback string? ----@return GitStatus -function M.git_status_file(parent_ignored, status, path, path_fallback) - if parent_ignored then - return { file = "!!" } - end +---Refresh contents and git status for a single directory +---@param dir DirectoryNode +function M.refresh_dir(dir) + local node = dir:get_parent_of_group() or dir + local toplevel = M.get_toplevel(dir.absolute_path) - if not status or not status.files then - return {} + M.reload_project(toplevel, dir.absolute_path, function() + local project = M.get_project(toplevel) or {} + + dir.explorer:reload(node, project) + + M.update_parent_projects(dir, project, toplevel) + + dir.explorer.renderer:draw() + end) +end + +---@param dir DirectoryNode? +---@param projects GitProject[] +function M.reload_node_status(dir, projects) + dir = dir and dir:as(DirectoryNode) + if not dir or #dir.nodes == 0 then + return end - return { - file = status.files[path] or status.files[path_fallback] - } + local toplevel = M.get_toplevel(dir.absolute_path) + local project = projects[toplevel] or {} + for _, node in ipairs(dir.nodes) do + node:update_git_status(dir:is_git_ignored(), project) + M.reload_node_status(node:as(DirectoryNode), projects) + end end function M.purge_state() diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua index 06235ea5e64..13b57383cf1 100644 --- a/lua/nvim-tree/git/runner.lua +++ b/lua/nvim-tree/git/runner.lua @@ -2,9 +2,21 @@ local log = require("nvim-tree.log") local utils = require("nvim-tree.utils") local notify = require("nvim-tree.notify") ----@class Runner -local Runner = {} -Runner.__index = Runner +local Class = require("nvim-tree.class") + +---@class (exact) GitRunnerOpts +---@field toplevel string absolute path +---@field path string? absolute path +---@field list_untracked boolean +---@field list_ignored boolean +---@field timeout integer +---@field callback fun(path_xy: GitPathXY)? + +---@class (exact) GitRunner: Class +---@field private opts GitRunnerOpts +---@field private path_xy GitPathXY +---@field private rc integer? -- -1 indicates timeout +local GitRunner = Class:new() local timeouts = 0 local MAX_TIMEOUTS = 5 @@ -12,7 +24,7 @@ local MAX_TIMEOUTS = 5 ---@private ---@param status string ---@param path string|nil -function Runner:_parse_status_output(status, path) +function GitRunner:parse_status_output(status, path) if not path then return end @@ -22,7 +34,7 @@ function Runner:_parse_status_output(status, path) path = path:gsub("/", "\\") end if #status > 0 and #path > 0 then - self.output[utils.path_remove_trailing(utils.path_join({ self.toplevel, path }))] = status + self.path_xy[utils.path_remove_trailing(utils.path_join({ self.opts.toplevel, path }))] = status end end @@ -30,7 +42,7 @@ end ---@param prev_output string ---@param incoming string ---@return string -function Runner:_handle_incoming_data(prev_output, incoming) +function GitRunner:handle_incoming_data(prev_output, incoming) if incoming and utils.str_find(incoming, "\n") then local prev = prev_output .. incoming local i = 1 @@ -45,7 +57,7 @@ function Runner:_handle_incoming_data(prev_output, incoming) -- skip next line if it is a rename entry skip_next_line = true end - self:_parse_status_output(status, path) + self:parse_status_output(status, path) end i = i + #line end @@ -58,35 +70,38 @@ function Runner:_handle_incoming_data(prev_output, incoming) end for line in prev_output:gmatch("[^\n]*\n") do - self:_parse_status_output(line) + self:parse_status_output(line) end return "" end +---@private ---@param stdout_handle uv.uv_pipe_t ---@param stderr_handle uv.uv_pipe_t ----@return table -function Runner:_getopts(stdout_handle, stderr_handle) - local untracked = self.list_untracked and "-u" or nil - local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no" +---@return uv.spawn.options +function GitRunner:get_spawn_options(stdout_handle, stderr_handle) + local untracked = self.opts.list_untracked and "-u" or nil + local ignored = (self.opts.list_untracked and self.opts.list_ignored) and "--ignored=matching" or "--ignored=no" return { - args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path }, - cwd = self.toplevel, + args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.opts.path }, + cwd = self.opts.toplevel, stdio = { nil, stdout_handle, stderr_handle }, } end +---@private ---@param output string -function Runner:_log_raw_output(output) +function GitRunner:log_raw_output(output) if log.enabled("git") and output and type(output) == "string" then log.raw("git", "%s", output) log.line("git", "done") end end +---@private ---@param callback function|nil -function Runner:_run_git_job(callback) +function GitRunner:run_git_job(callback) local handle, pid local stdout = vim.loop.new_pipe(false) local stderr = vim.loop.new_pipe(false) @@ -123,20 +138,20 @@ function Runner:_run_git_job(callback) end end - local opts = self:_getopts(stdout, stderr) - log.line("git", "running job with timeout %dms", self.timeout) - log.line("git", "git %s", table.concat(utils.array_remove_nils(opts.args), " ")) + local spawn_options = self:get_spawn_options(stdout, stderr) + log.line("git", "running job with timeout %dms", self.opts.timeout) + log.line("git", "git %s", table.concat(utils.array_remove_nils(spawn_options.args), " ")) handle, pid = vim.loop.spawn( "git", - opts, + spawn_options, vim.schedule_wrap(function(rc) on_finish(rc) end) ) timer:start( - self.timeout, + self.opts.timeout, 0, vim.schedule_wrap(function() on_finish(-1) @@ -151,19 +166,20 @@ function Runner:_run_git_job(callback) if data then data = data:gsub("%z", "\n") end - self:_log_raw_output(data) - output_leftover = self:_handle_incoming_data(output_leftover, data) + self:log_raw_output(data) + output_leftover = self:handle_incoming_data(output_leftover, data) end local function manage_stderr(_, data) - self:_log_raw_output(data) + self:log_raw_output(data) end vim.loop.read_start(stdout, vim.schedule_wrap(manage_stdout)) vim.loop.read_start(stderr, vim.schedule_wrap(manage_stderr)) end -function Runner:_wait() +---@private +function GitRunner:wait() local function is_done() return self.rc ~= nil end @@ -172,64 +188,69 @@ function Runner:_wait() end end ----@param opts table -function Runner:_finalise(opts) +---@private +function GitRunner:finalise() if self.rc == -1 then - log.line("git", "job timed out %s %s", opts.toplevel, opts.path) + log.line("git", "job timed out %s %s", self.opts.toplevel, self.opts.path) timeouts = timeouts + 1 if timeouts == MAX_TIMEOUTS then - notify.warn(string.format("%d git jobs have timed out after git.timeout %dms, disabling git integration.", timeouts, opts.timeout)) + notify.warn(string.format("%d git jobs have timed out after git.timeout %dms, disabling git integration.", timeouts, + self.opts.timeout)) require("nvim-tree.git").disable_git_integration() end elseif self.rc ~= 0 then - log.line("git", "job fail rc %d %s %s", self.rc, opts.toplevel, opts.path) + log.line("git", "job fail rc %d %s %s", self.rc, self.opts.toplevel, self.opts.path) else - log.line("git", "job success %s %s", opts.toplevel, opts.path) + log.line("git", "job success %s %s", self.opts.toplevel, self.opts.path) end end ---- Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms ----@param opts table ----@param callback function|nil executed passing return when complete ----@return table|nil status by absolute path, nil if callback present -function Runner.run(opts, callback) - local self = setmetatable({ - toplevel = opts.toplevel, - path = opts.path, - list_untracked = opts.list_untracked, - list_ignored = opts.list_ignored, - timeout = opts.timeout or 400, - output = {}, - rc = nil, -- -1 indicates timeout - }, Runner) - - local async = callback ~= nil - local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", opts.toplevel, opts.path) - - if async and callback then +---Return nil when callback present +---@private +---@return GitPathXY? +function GitRunner:execute() + local async = self.opts.callback ~= nil + local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", self.opts.toplevel, self.opts.path) + + if async and self.opts.callback then -- async, always call back - self:_run_git_job(function() + self:run_git_job(function() log.profile_end(profile) - self:_finalise(opts) + self:finalise() - callback(self.output) + self.opts.callback(self.path_xy) end) else -- sync, maybe call back - self:_run_git_job() - self:_wait() + self:run_git_job() + self:wait() log.profile_end(profile) - self:_finalise(opts) + self:finalise() - if callback then - callback(self.output) + if self.opts.callback then + self.opts.callback(self.path_xy) else - return self.output + return self.path_xy end end end -return Runner +---Static method to run a git process, which will be killed if it takes more than timeout +---Return nil when callback present +---@param opts GitRunnerOpts +---@return GitPathXY? +function GitRunner:run(opts) + ---@type GitRunner + local runner = { + opts = opts, + path_xy = {}, + } + runner = GitRunner:new(runner) + + return runner:execute() +end + +return GitRunner diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua index 6a32b632e31..a5a1efc21b1 100644 --- a/lua/nvim-tree/git/utils.lua +++ b/lua/nvim-tree/git/utils.lua @@ -58,10 +58,11 @@ function M.get_toplevel(cwd) return toplevel, git_dir end +---@type table local untracked = {} ---@param cwd string ----@return string|nil +---@return boolean function M.should_show_untracked(cwd) if untracked[cwd] ~= nil then return untracked[cwd] @@ -81,8 +82,8 @@ function M.should_show_untracked(cwd) return untracked[cwd] end ----@param t table|nil ----@param k string +---@param t table? +---@param k string|integer ---@return table local function nil_insert(t, k) t = t or {} @@ -90,31 +91,33 @@ local function nil_insert(t, k) return t end ----@param status table +---@param project_files GitProjectFiles ---@param cwd string|nil ----@return table -function M.file_status_to_dir_status(status, cwd) - local direct = {} - for p, s in pairs(status) do +---@return GitProjectDirs +function M.project_files_to_project_dirs(project_files, cwd) + ---@type GitProjectDirs + local project_dirs = {} + + project_dirs.direct = {} + for p, s in pairs(project_files) do if s ~= "!!" then local modified = vim.fn.fnamemodify(p, ":h") - direct[modified] = nil_insert(direct[modified], s) + project_dirs.direct[modified] = nil_insert(project_dirs.direct[modified], s) end end - local indirect = {} - for dirname, statuses in pairs(direct) do + project_dirs.indirect = {} + for dirname, statuses in pairs(project_dirs.direct) do for s, _ in pairs(statuses) do local modified = dirname while modified ~= cwd and modified ~= "/" do modified = vim.fn.fnamemodify(modified, ":h") - indirect[modified] = nil_insert(indirect[modified], s) + project_dirs.indirect[modified] = nil_insert(project_dirs.indirect[modified], s) end end end - local r = { indirect = indirect, direct = direct } - for _, d in pairs(r) do + for _, d in pairs(project_dirs) do for dirname, statuses in pairs(d) do local new_statuses = {} for s, _ in pairs(statuses) do @@ -123,7 +126,60 @@ function M.file_status_to_dir_status(status, cwd) d[dirname] = new_statuses end end - return r + + return project_dirs +end + +---Git file status for an absolute path +---@param parent_ignored boolean +---@param project GitProject? +---@param path string +---@param path_fallback string? alternative file path when no other file status +---@return GitNodeStatus +function M.git_status_file(parent_ignored, project, path, path_fallback) + ---@type GitNodeStatus + local ns + + if parent_ignored then + ns = { + file = "!!" + } + elseif project and project.files then + ns = { + file = project.files[path] or project.files[path_fallback] + } + else + ns = {} + end + + return ns +end + +---Git file and directory status for an absolute path +---@param parent_ignored boolean +---@param project GitProject? +---@param path string +---@param path_fallback string? alternative file path when no other file status +---@return GitNodeStatus? +function M.git_status_dir(parent_ignored, project, path, path_fallback) + ---@type GitNodeStatus? + local ns + + if parent_ignored then + ns = { + file = "!!" + } + elseif project then + ns = { + file = project.files and (project.files[path] or project.files[path_fallback]), + dir = project.dirs and { + direct = project.dirs.direct and project.dirs.direct[path], + indirect = project.dirs.indirect and project.dirs.indirect[path], + }, + } + end + + return ns end function M.setup(opts) diff --git a/lua/nvim-tree/log.lua b/lua/nvim-tree/log.lua index bfb968ae5ca..9665c1335e6 100644 --- a/lua/nvim-tree/log.lua +++ b/lua/nvim-tree/log.lua @@ -1,7 +1,12 @@ -local M = { - config = nil, - path = nil, -} +---@alias LogTypes "all" | "config" | "copy_paste" | "dev" | "diagnostics" | "git" | "profile" | "watcher" + +---@type table +local types = {} + +---@type string +local file_path + +local M = {} --- Write to log file ---@param typ string as per log.types config @@ -13,7 +18,7 @@ function M.raw(typ, fmt, ...) end local line = string.format(fmt, ...) - local file = io.open(M.path, "a") + local file = io.open(file_path, "a") if file then io.output(file) io.write(line) @@ -22,7 +27,7 @@ function M.raw(typ, fmt, ...) end --- Write to a new file ----@param typ string as per log.types config +---@param typ LogTypes as per log.types config ---@param path string absolute path ---@param fmt string for string.format ---@param ... any arguments for string.format @@ -71,7 +76,7 @@ end --- Write to log file --- time and typ are prefixed and a trailing newline is added ----@param typ string as per log.types config +---@param typ LogTypes as per log.types config ---@param fmt string for string.format ---@param ... any arguments for string.format function M.line(typ, fmt, ...) @@ -88,7 +93,7 @@ function M.set_inspect_opts(opts) end --- Write to log file the inspection of a node ----@param typ string as per log.types config +---@param typ LogTypes as per log.types config ---@param node Node node to be inspected ---@param fmt string for string.format ---@param ... any arguments for string.format @@ -99,20 +104,20 @@ function M.node(typ, node, fmt, ...) end --- Logging is enabled for typ or all ----@param typ string as per log.types config +---@param typ LogTypes as per log.types config ---@return boolean function M.enabled(typ) - return M.path ~= nil and (M.config.types[typ] or M.config.types.all) + return file_path ~= nil and (types[typ] or types.all) end function M.setup(opts) - M.config = opts.log - if M.config and M.config.enable and M.config.types then - M.path = string.format("%s/nvim-tree.log", vim.fn.stdpath("log"), os.date("%H:%M:%S"), vim.env.USER) - if M.config.truncate then - os.remove(M.path) + if opts.log and opts.log.enable and opts.log.types then + types = opts.log.types + file_path = string.format("%s/nvim-tree.log", vim.fn.stdpath("log"), os.date("%H:%M:%S"), vim.env.USER) + if opts.log.truncate then + os.remove(file_path) end - require("nvim-tree.notify").debug("nvim-tree.lua logging to " .. M.path) + require("nvim-tree.notify").debug("nvim-tree.lua logging to " .. file_path) end end diff --git a/lua/nvim-tree/marks/init.lua b/lua/nvim-tree/marks/init.lua index fde1e534f3b..ddeb5cc9200 100644 --- a/lua/nvim-tree/marks/init.lua +++ b/lua/nvim-tree/marks/init.lua @@ -200,7 +200,8 @@ function Marks:navigate(up) Iterator.builder(self.explorer.nodes) :recursor(function(n) - return n.open and n.nodes + local dir = n:as(DirectoryNode) + return dir and dir.open and dir.nodes end) :applier(function(n) if n.absolute_path == node.absolute_path then @@ -263,7 +264,7 @@ function Marks:navigate_select() return end local node = self.marks[choice] - if node and not node.nodes and not utils.get_win_buf_from_path(node.absolute_path) then + if node and not node:is(DirectoryNode) and not utils.get_win_buf_from_path(node.absolute_path) then open_file.fn("edit", node.absolute_path) elseif node then utils.focus_file(node.absolute_path) diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua index 0e6fd338297..8cfa3760a9e 100644 --- a/lua/nvim-tree/node/directory-link.lua +++ b/lua/nvim-tree/node/directory-link.lua @@ -1,10 +1,11 @@ -local git = require("nvim-tree.git") +local git_utils = require("nvim-tree.git.utils") +local utils = require("nvim-tree.utils") local DirectoryNode = require("nvim-tree.node.directory") ---@class (exact) DirectoryLinkNode: DirectoryNode ---@field link_to string absolute path ----@field fs_stat_target uv.fs_stat.result +---@field private fs_stat_target uv.fs_stat.result local DirectoryLinkNode = DirectoryNode:new() ---Static factory method @@ -20,7 +21,7 @@ function DirectoryLinkNode:create(explorer, parent, absolute_path, link_to, name -- create DirectoryNode with the target path for the watcher local o = DirectoryNode:create(explorer, parent, link_to, name, fs_stat) - o = self:new(o) --[[@as DirectoryLinkNode]] + o = self:new(o) -- reset absolute path to the link itself o.absolute_path = absolute_path @@ -36,11 +37,44 @@ function DirectoryLinkNode:destroy() DirectoryNode.destroy(self) end ------Update the directory GitStatus of link target and the file status of the link itself ------@param parent_ignored boolean ------@param status table|nil -function DirectoryLinkNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_dir(parent_ignored, status, self.link_to, self.absolute_path) +---Update the directory git_status of link target and the file status of the link itself +---@param parent_ignored boolean +---@param project GitProject? +function DirectoryLinkNode:update_git_status(parent_ignored, project) + self.git_status = git_utils.git_status_dir(parent_ignored, project, self.link_to, self.absolute_path) +end + +---@return HighlightedString name +function DirectoryLinkNode:highlighted_icon() + if not self.explorer.opts.renderer.icons.show.folder then + return self:highlighted_icon_empty() + end + + local str, hl + + if self.open then + str = self.explorer.opts.renderer.icons.glyphs.folder.symlink_open + hl = "NvimTreeOpenedFolderIcon" + else + str = self.explorer.opts.renderer.icons.glyphs.folder.symlink + hl = "NvimTreeClosedFolderIcon" + end + + return { str = str, hl = { hl } } +end + +---Maybe override name with arrow +---@return HighlightedString name +function DirectoryLinkNode:highlighted_name() + local name = DirectoryNode.highlighted_name(self) + + if self.explorer.opts.renderer.symlink_destination then + local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path) + name.str = string.format("%s%s%s", name.str, self.explorer.opts.renderer.icons.symlink_arrow, link_to) + name.hl = { "NvimTreeSymlinkFolderName" } + end + + return name end ---Create a sanitized partial copy of a node, populating children recursively. diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua index 0e372705f2a..3d87465aa04 100644 --- a/lua/nvim-tree/node/directory.lua +++ b/lua/nvim-tree/node/directory.lua @@ -1,6 +1,6 @@ -local git = require("nvim-tree.git") -local watch = require("nvim-tree.explorer.watch") - +local git_utils = require("nvim-tree.git.utils") +local icons = require("nvim-tree.renderer.components.devicons") +local notify = require("nvim-tree.notify") local Node = require("nvim-tree.node") ---@class (exact) DirectoryNode: Node @@ -8,8 +8,8 @@ local Node = require("nvim-tree.node") ---@field group_next DirectoryNode? -- If node is grouped, this points to the next child dir/link node ---@field nodes Node[] ---@field open boolean ----@field watcher Watcher? ---@field hidden_stats table? -- Each field of this table is a key for source and value for count +---@field private watcher Watcher? local DirectoryNode = Node:new() ---Static factory method @@ -32,11 +32,11 @@ function DirectoryNode:create(explorer, parent, absolute_path, name, fs_stat) fs_stat = fs_stat, git_status = nil, hidden = false, - is_dot = false, name = name, parent = parent, watcher = nil, diag_status = nil, + is_dot = false, has_children = has_children, group_next = nil, @@ -44,9 +44,9 @@ function DirectoryNode:create(explorer, parent, absolute_path, name, fs_stat) open = false, hidden_stats = nil, } - o = self:new(o) --[[@as DirectoryNode]] + o = self:new(o) - o.watcher = watch.create_watcher(o) + o.watcher = require("nvim-tree.explorer.watch").create_watcher(o) return o end @@ -66,41 +66,41 @@ function DirectoryNode:destroy() Node.destroy(self) end ----Update the GitStatus of the directory +---Update the git_status of the directory ---@param parent_ignored boolean ----@param status table|nil -function DirectoryNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_dir(parent_ignored, status, self.absolute_path, nil) +---@param project GitProject? +function DirectoryNode:update_git_status(parent_ignored, project) + self.git_status = git_utils.git_status_dir(parent_ignored, project, self.absolute_path, nil) end ----@return GitStatus|nil -function DirectoryNode:get_git_status() +---@return GitXY[]? +function DirectoryNode:get_git_xy() if not self.git_status or not self.explorer.opts.git.show_on_dirs then return nil end - local status = {} + local xys = {} if not self:last_group_node().open or self.explorer.opts.git.show_on_open_dirs then -- dir is closed or we should show on open_dirs if self.git_status.file ~= nil then - table.insert(status, self.git_status.file) + table.insert(xys, self.git_status.file) end if self.git_status.dir ~= nil then if self.git_status.dir.direct ~= nil then for _, s in pairs(self.git_status.dir.direct) do - table.insert(status, s) + table.insert(xys, s) end end if self.git_status.dir.indirect ~= nil then for _, s in pairs(self.git_status.dir.indirect) do - table.insert(status, s) + table.insert(xys, s) end end end else -- dir is open and we shouldn't show on open_dirs if self.git_status.file ~= nil then - table.insert(status, self.git_status.file) + table.insert(xys, self.git_status.file) end if self.git_status.dir ~= nil and self.git_status.dir.direct ~= nil then local deleted = { @@ -111,34 +111,18 @@ function DirectoryNode:get_git_status() } for _, s in pairs(self.git_status.dir.direct) do if deleted[s] then - table.insert(status, s) + table.insert(xys, s) end end end end - if #status == 0 then + if #xys == 0 then return nil else - return status + return xys end end ----Refresh contents and git status for a single node -function DirectoryNode:refresh() - local node = self:get_parent_of_group() or self - local toplevel = git.get_toplevel(self.absolute_path) - - git.reload_project(toplevel, self.absolute_path, function() - local project = git.get_project(toplevel) or {} - - self.explorer:reload(node, project) - - node:update_parent_statuses(project, toplevel) - - self.explorer.renderer:draw() - end) -end - -- If node is grouped, return the last node in the group. Otherwise, return the given node. ---@return DirectoryNode function DirectoryNode:last_group_node() @@ -191,7 +175,7 @@ function DirectoryNode:ungroup_empty_folders() end end ----@param toggle_group boolean +---@param toggle_group boolean? function DirectoryNode:expand_or_collapse(toggle_group) toggle_group = toggle_group or false if self.has_children then @@ -224,6 +208,84 @@ function DirectoryNode:expand_or_collapse(toggle_group) self.explorer.renderer:draw() end +---@return HighlightedString icon +function DirectoryNode:highlighted_icon() + if not self.explorer.opts.renderer.icons.show.folder then + return self:highlighted_icon_empty() + end + + local str, hl + + -- devicon if enabled and available + if self.explorer.opts.renderer.icons.web_devicons.folder.enable then + str, hl = icons.get_icon(self.name) + if not self.explorer.opts.renderer.icons.web_devicons.folder.color then + hl = nil + end + end + + -- default icon from opts + if not str then + if #self.nodes ~= 0 or self.has_children then + if self.open then + str = self.explorer.opts.renderer.icons.glyphs.folder.open + else + str = self.explorer.opts.renderer.icons.glyphs.folder.default + end + else + if self.open then + str = self.explorer.opts.renderer.icons.glyphs.folder.empty_open + else + str = self.explorer.opts.renderer.icons.glyphs.folder.empty + end + end + end + + -- default hl + if not hl then + if self.open then + hl = "NvimTreeOpenedFolderIcon" + else + hl = "NvimTreeClosedFolderIcon" + end + end + + return { str = str, hl = { hl } } +end + +---@return HighlightedString icon +function DirectoryNode:highlighted_name() + local str, hl + + local name = self.name + local next = self.group_next + while next do + name = string.format("%s/%s", name, next.name) + next = next.group_next + end + + if self.group_next and type(self.explorer.opts.renderer.group_empty) == "function" then + local new_name = self.explorer.opts.renderer.group_empty(name) + if type(new_name) == "string" then + name = new_name + else + notify.warn(string.format("Invalid return type for field renderer.group_empty. Expected string, got %s", type(new_name))) + end + end + str = string.format("%s%s", name, self.explorer.opts.renderer.add_trailing and "/" or "") + + hl = "NvimTreeFolderName" + if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then + hl = "NvimTreeSpecialFolderName" + elseif self.open then + hl = "NvimTreeOpenedFolderName" + elseif #self.nodes == 0 and not self.has_children then + hl = "NvimTreeEmptyFolderName" + end + + return { str = str, hl = { hl } } +end + ---Create a sanitized partial copy of a node, populating children recursively. ---@return DirectoryNode cloned function DirectoryNode:clone() diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua index 2d2571f01a8..0c168a50a93 100644 --- a/lua/nvim-tree/node/file-link.lua +++ b/lua/nvim-tree/node/file-link.lua @@ -1,10 +1,11 @@ -local git = require("nvim-tree.git") +local git_utils = require("nvim-tree.git.utils") +local utils = require("nvim-tree.utils") local FileNode = require("nvim-tree.node.file") ---@class (exact) FileLinkNode: FileNode ---@field link_to string absolute path ----@field fs_stat_target uv.fs_stat.result +---@field private fs_stat_target uv.fs_stat.result local FileLinkNode = FileNode:new() ---Static factory method @@ -19,7 +20,7 @@ local FileLinkNode = FileNode:new() function FileLinkNode:create(explorer, parent, absolute_path, link_to, name, fs_stat, fs_stat_target) local o = FileNode:create(explorer, parent, absolute_path, name, fs_stat) - o = self:new(o) --[[@as FileLinkNode]] + o = self:new(o) o.type = "link" o.link_to = link_to @@ -32,11 +33,37 @@ function FileLinkNode:destroy() FileNode.destroy(self) end ------Update the GitStatus of the target otherwise the link itself ------@param parent_ignored boolean ------@param status table|nil -function FileLinkNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_file(parent_ignored, status, self.link_to, self.absolute_path) +---Update the git_status of the target otherwise the link itself +---@param parent_ignored boolean +---@param project GitProject? +function FileLinkNode:update_git_status(parent_ignored, project) + self.git_status = git_utils.git_status_file(parent_ignored, project, self.link_to, self.absolute_path) +end + +---@return HighlightedString icon +function FileLinkNode:highlighted_icon() + if not self.explorer.opts.renderer.icons.show.file then + return self:highlighted_icon_empty() + end + + local str, hl + + -- default icon from opts + str = self.explorer.opts.renderer.icons.glyphs.symlink + hl = "NvimTreeSymlinkIcon" + + return { str = str, hl = { hl } } +end + +---@return HighlightedString name +function FileLinkNode:highlighted_name() + local str = self.name + if self.explorer.opts.renderer.symlink_destination then + local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path) + str = string.format("%s%s%s", str, self.explorer.opts.renderer.icons.symlink_arrow, link_to) + end + + return { str = str, hl = { "NvimTreeSymlink" } } end ---Create a sanitized partial copy of a node diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua index 398607c6c9a..18555fef26c 100644 --- a/lua/nvim-tree/node/file.lua +++ b/lua/nvim-tree/node/file.lua @@ -1,8 +1,18 @@ -local git = require("nvim-tree.git") +local git_utils = require("nvim-tree.git.utils") +local icons = require("nvim-tree.renderer.components.devicons") local utils = require("nvim-tree.utils") local Node = require("nvim-tree.node") +local PICTURE_MAP = { + jpg = true, + jpeg = true, + png = true, + gif = true, + webp = true, + jxl = true, +} + ---@class (exact) FileNode: Node ---@field extension string local FileNode = Node:new() @@ -24,14 +34,14 @@ function FileNode:create(explorer, parent, absolute_path, name, fs_stat) fs_stat = fs_stat, git_status = nil, hidden = false, - is_dot = false, name = name, parent = parent, diag_status = nil, + is_dot = false, extension = string.match(name, ".?[^.]+%.(.*)") or "", } - o = self:new(o) --[[@as FileNode]] + o = self:new(o) return o end @@ -42,13 +52,13 @@ end ---Update the GitStatus of the file ---@param parent_ignored boolean ----@param status table|nil -function FileNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_file(parent_ignored, status, self.absolute_path, nil) +---@param project GitProject? +function FileNode:update_git_status(parent_ignored, project) + self.git_status = git_utils.git_status_file(parent_ignored, project, self.absolute_path, nil) end ----@return GitStatus|nil -function FileNode:get_git_status() +---@return GitXY[]? +function FileNode:get_git_xy() if not self.git_status then return nil end @@ -56,6 +66,49 @@ function FileNode:get_git_status() return self.git_status.file and { self.git_status.file } end +---@return HighlightedString icon +function FileNode:highlighted_icon() + if not self.explorer.opts.renderer.icons.show.file then + return self:highlighted_icon_empty() + end + + local str, hl + + -- devicon if enabled and available, fallback to default + if self.explorer.opts.renderer.icons.web_devicons.file.enable then + str, hl = icons.get_icon(self.name, nil, { default = true }) + if not self.explorer.opts.renderer.icons.web_devicons.file.color then + hl = nil + end + end + + -- default icon from opts + if not str then + str = self.explorer.opts.renderer.icons.glyphs.default + end + + -- default hl + if not hl then + hl = "NvimTreeFileIcon" + end + + return { str = str, hl = { hl } } +end + +---@return HighlightedString name +function FileNode:highlighted_name() + local hl + if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then + hl = "NvimTreeSpecialFile" + elseif self.executable then + hl = "NvimTreeExecFile" + elseif PICTURE_MAP[self.extension] then + hl = "NvimTreeImageFile" + end + + return { str = self.name, hl = { hl } } +end + ---Create a sanitized partial copy of a node ---@return FileNode cloned function FileNode:clone() diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua index 5b7f7b1a601..48bf783c421 100644 --- a/lua/nvim-tree/node/init.lua +++ b/lua/nvim-tree/node/init.lua @@ -1,5 +1,3 @@ -local git = require("nvim-tree.git") - local Class = require("nvim-tree.class") ---Abstract Node class. @@ -10,41 +8,28 @@ local Class = require("nvim-tree.class") ---@field absolute_path string ---@field executable boolean ---@field fs_stat uv.fs_stat.result? ----@field git_status GitStatus? +---@field git_status GitNodeStatus? ---@field hidden boolean ---@field name string ---@field parent DirectoryNode? ---@field diag_status DiagStatus? ----@field is_dot boolean cached is_dotfile +---@field private is_dot boolean cached is_dotfile local Node = Class:new() function Node:destroy() end ---luacheck: push ignore 212 ----Update the GitStatus of the node +---Update the git_status of the node +---Abstract ---@param parent_ignored boolean ----@param status table? -function Node:update_git_status(parent_ignored, status) ---@diagnostic disable-line: unused-local - ---TODO find a way to declare abstract methods -end - ---luacheck: pop - ----@return GitStatus? -function Node:get_git_status() +---@param project GitProject? +function Node:update_git_status(parent_ignored, project) + self:nop(parent_ignored, project) end ----@param projects table -function Node:reload_node_status(projects) - local toplevel = git.get_toplevel(self.absolute_path) - local status = projects[toplevel] or {} - for _, node in ipairs(self.nodes) do - node:update_git_status(self:is_git_ignored(), status) - if node.nodes and #node.nodes > 0 then - node:reload_node_status(projects) - end - end +---Short-format statuses +---@return GitXY[]? +function Node:get_git_xy() end ---@return boolean @@ -66,38 +51,6 @@ function Node:is_dotfile() return false end ----@param project table? ----@param root string? -function Node:update_parent_statuses(project, root) - local node = self - while project and node do - -- step up to the containing project - if node.absolute_path == root then - -- stop at the top of the tree - if not node.parent then - break - end - - root = git.get_toplevel(node.parent.absolute_path) - - -- stop when no more projects - if not root then - break - end - - -- update the containing project - project = git.get_project(root) - git.reload_project(root, node.absolute_path, nil) - end - - -- update status - node:update_git_status(node.parent and node.parent:is_git_ignored() or false, project) - - -- maybe parent - node = node.parent - end -end - ---Get the highest parent of grouped nodes, nil when not grouped ---@return DirectoryNode? function Node:get_parent_of_group() @@ -115,6 +68,34 @@ function Node:get_parent_of_group() end end +---Empty highlighted icon +---@protected +---@return HighlightedString icon +function Node:highlighted_icon_empty() + return { str = "", hl = {} } +end + +---Highlighted icon for the node +---Empty for base Node +---@return HighlightedString icon +function Node:highlighted_icon() + return self:highlighted_icon_empty() +end + +---Empty highlighted name +---@protected +---@return HighlightedString name +function Node:highlighted_name_empty() + return { str = "", hl = {} } +end + +---Highlighted name for the node +---Empty for base Node +---@return HighlightedString icon +function Node:highlighted_name() + return self:highlighted_name_empty() +end + ---Create a sanitized partial copy of a node, populating children recursively. ---@return Node cloned function Node:clone() @@ -130,10 +111,10 @@ function Node:clone() fs_stat = self.fs_stat, git_status = self.git_status, hidden = self.hidden, - is_dot = self.is_dot, name = self.name, parent = nil, diag_status = nil, + is_dot = self.is_dot, } return clone diff --git a/lua/nvim-tree/node/root.lua b/lua/nvim-tree/node/root.lua index 2fd037cecf5..0544a141a12 100644 --- a/lua/nvim-tree/node/root.lua +++ b/lua/nvim-tree/node/root.lua @@ -12,7 +12,7 @@ local RootNode = DirectoryNode:new() function RootNode:create(explorer, absolute_path, name, fs_stat) local o = DirectoryNode:create(explorer, nil, absolute_path, name, fs_stat) - o = self:new(o) --[[@as RootNode]] + o = self:new(o) return o end diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua index 9ae25070913..37359269822 100644 --- a/lua/nvim-tree/renderer/builder.lua +++ b/lua/nvim-tree/renderer/builder.lua @@ -2,9 +2,7 @@ local notify = require("nvim-tree.notify") local utils = require("nvim-tree.utils") local view = require("nvim-tree.view") -local DirectoryLinkNode = require("nvim-tree.node.directory-link") local DirectoryNode = require("nvim-tree.node.directory") -local FileLinkNode = require("nvim-tree.node.file-link") local DecoratorBookmarks = require("nvim-tree.renderer.decorator.bookmarks") local DecoratorCopied = require("nvim-tree.renderer.decorator.copied") @@ -16,16 +14,6 @@ local DecoratorHidden = require("nvim-tree.renderer.decorator.hidden") local DecoratorOpened = require("nvim-tree.renderer.decorator.opened") local pad = require("nvim-tree.renderer.components.padding") -local icons = require("nvim-tree.renderer.components.icons") - -local PICTURE_MAP = { - jpg = true, - jpeg = true, - png = true, - gif = true, - webp = true, - jxl = true, -} ---@class (exact) HighlightedString ---@field str string @@ -45,6 +33,7 @@ local PICTURE_MAP = { ---@field extmarks table[] extra marks for right icon placement ---@field virtual_lines table[] virtual lines for hidden count display ---@field private explorer Explorer +---@field private opts table ---@field private index number ---@field private depth number ---@field private combined_groups table combined group names @@ -99,27 +88,6 @@ function Builder:insert_highlight(groups, start, end_) table.insert(self.hl_args, { groups, self.index, start, end_ or -1 }) end ----@private -function Builder:get_folder_name(node) - local name = node.name - local next = node.group_next - while next do - name = string.format("%s/%s", name, next.name) - next = next.group_next - end - - if node.group_next and type(self.opts.renderer.group_empty) == "function" then - local new_name = self.opts.renderer.group_empty(name) - if type(new_name) == "string" then - name = new_name - else - notify.warn(string.format("Invalid return type for field renderer.group_empty. Expected string, got %s", type(new_name))) - end - end - - return string.format("%s%s", name, self.opts.renderer.add_trailing and "/" or "") -end - ---@private ---@param highlighted_strings HighlightedString[] ---@return string @@ -140,82 +108,6 @@ function Builder:unwrap_highlighted_strings(highlighted_strings) return string end ----@private ----@param node Node ----@return HighlightedString icon ----@return HighlightedString name -function Builder:build_folder(node) - local has_children = #node.nodes ~= 0 or node.has_children - local icon, icon_hl = icons.get_folder_icon(node, has_children) - local foldername = self:get_folder_name(node) - - if #icon > 0 and icon_hl == nil then - if node.open then - icon_hl = "NvimTreeOpenedFolderIcon" - else - icon_hl = "NvimTreeClosedFolderIcon" - end - end - - local foldername_hl = "NvimTreeFolderName" - if node.link_to and self.opts.renderer.symlink_destination then - local arrow = icons.i.symlink_arrow - local link_to = utils.path_relative(node.link_to, self.explorer.absolute_path) - foldername = string.format("%s%s%s", foldername, arrow, link_to) - foldername_hl = "NvimTreeSymlinkFolderName" - elseif - vim.tbl_contains(self.opts.renderer.special_files, node.absolute_path) or vim.tbl_contains(self.opts.renderer.special_files, node.name) - then - foldername_hl = "NvimTreeSpecialFolderName" - elseif node.open then - foldername_hl = "NvimTreeOpenedFolderName" - elseif not has_children then - foldername_hl = "NvimTreeEmptyFolderName" - end - - return { str = icon, hl = { icon_hl } }, { str = foldername, hl = { foldername_hl } } -end - ----@private ----@param node table ----@return HighlightedString icon ----@return HighlightedString name -function Builder:build_symlink(node) - local icon = icons.i.symlink - local arrow = icons.i.symlink_arrow - local symlink_formatted = node.name - if self.opts.renderer.symlink_destination then - local link_to = utils.path_relative(node.link_to, self.explorer.absolute_path) - symlink_formatted = string.format("%s%s%s", symlink_formatted, arrow, link_to) - end - - if self.opts.renderer.icons.show.file then - return { str = icon, hl = { "NvimTreeSymlinkIcon" } }, { str = symlink_formatted, hl = { "NvimTreeSymlink" } } - else - return { str = "", hl = {} }, { str = symlink_formatted, hl = { "NvimTreeSymlink" } } - end -end - ----@private ----@param node Node ----@return HighlightedString icon ----@return HighlightedString name -function Builder:build_file(node) - local hl - if - vim.tbl_contains(self.opts.renderer.special_files, node.absolute_path) or vim.tbl_contains(self.opts.renderer.special_files, node.name) - then - hl = "NvimTreeSpecialFile" - elseif node.executable then - hl = "NvimTreeExecFile" - elseif PICTURE_MAP[node.extension] then - hl = "NvimTreeImageFile" - end - - local icon, hl_group = icons.get_file_icon(node.name, node.extension) - return { str = icon, hl = { hl_group } }, { str = node.name, hl = { hl } } -end - ---@private ---@param indent_markers HighlightedString[] ---@param arrows HighlightedString[]|nil @@ -360,14 +252,7 @@ function Builder:build_line(node, idx, num_children) local arrows = pad.get_arrows(node) -- main components - local icon, name - if node:is(DirectoryNode) then - icon, name = self:build_folder(node) - elseif node:is(DirectoryLinkNode) or node:is(FileLinkNode) then - icon, name = self:build_symlink(node) - else - icon, name = self:build_file(node) - end + local icon, name = node:highlighted_icon(), node:highlighted_name() -- highighting local icon_hl_group, name_hl_group = self:add_highlights(node) @@ -379,11 +264,12 @@ function Builder:build_line(node, idx, num_children) self.index = self.index + 1 - if node:is(DirectoryNode) then - node = node:last_group_node() - if node.open then + local dir = node:as(DirectoryNode) + if dir then + dir = dir:last_group_node() + if dir.open then self.depth = self.depth + 1 - self:build_lines(node) + self:build_lines(dir) self.depth = self.depth - 1 end end diff --git a/lua/nvim-tree/renderer/components/devicons.lua b/lua/nvim-tree/renderer/components/devicons.lua new file mode 100644 index 00000000000..ad91d058c15 --- /dev/null +++ b/lua/nvim-tree/renderer/components/devicons.lua @@ -0,0 +1,35 @@ +---@alias devicons_get_icon fun(name: string, ext: string?, opts: table?): string?, string? +---@alias devicons_setup fun(opts: table?) + +---@class (strict) DevIcons? +---@field setup devicons_setup +---@field get_icon devicons_get_icon +local devicons + +local M = {} + +---Wrapper around nvim-web-devicons, nils if devicons not available +---@type devicons_get_icon +function M.get_icon(name, ext, opts) + if devicons then + return devicons.get_icon(name, ext, opts) + else + return nil, nil + end +end + +---Attempt to use nvim-web-devicons if present and enabled for file or folder +---@param opts table +function M.setup(opts) + if opts.renderer.icons.show.file or opts.renderer.icons.show.folder then + local ok, di = pcall(require, "nvim-web-devicons") + if ok then + devicons = di --[[@as DevIcons]] + + -- does nothing if already called i.e. doesn't clobber previous user setup + devicons.setup() + end + end +end + +return M diff --git a/lua/nvim-tree/renderer/components/diagnostics.lua b/lua/nvim-tree/renderer/components/diagnostics.lua index e51712e7746..e91873e07ca 100644 --- a/lua/nvim-tree/renderer/components/diagnostics.lua +++ b/lua/nvim-tree/renderer/components/diagnostics.lua @@ -1,6 +1,8 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION local diagnostics = require("nvim-tree.diagnostics") +local DirectoryNode = require("nvim-tree.node.directory") + local M = { -- highlight strings for the icons HS_ICON = {}, @@ -24,7 +26,7 @@ function M.get_highlight(node) local group local diag_status = diagnostics.get_diag_status(node) - if node.nodes then + if node:is(DirectoryNode) then group = M.HS_FOLDER[diag_status and diag_status.value] else group = M.HS_FILE[diag_status and diag_status.value] diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua deleted file mode 100644 index f2eed6837af..00000000000 --- a/lua/nvim-tree/renderer/components/icons.lua +++ /dev/null @@ -1,129 +0,0 @@ -local M = { i = {} } - -local function config_symlinks() - M.i.symlink = #M.config.glyphs.symlink > 0 and M.config.glyphs.symlink or "" - M.i.symlink_arrow = M.config.symlink_arrow -end - ----@return string icon ----@return string? name -local function empty() - return "", nil -end - ----@param node Node ----@param has_children boolean ----@return string icon ----@return string? name -local function get_folder_icon_default(node, has_children) - local is_symlink = node.link_to ~= nil - local n - if is_symlink and node.open then - n = M.config.glyphs.folder.symlink_open - elseif is_symlink then - n = M.config.glyphs.folder.symlink - elseif node.open then - if has_children then - n = M.config.glyphs.folder.open - else - n = M.config.glyphs.folder.empty_open - end - else - if has_children then - n = M.config.glyphs.folder.default - else - n = M.config.glyphs.folder.empty - end - end - return n, nil -end - ----@param node Node ----@param has_children boolean ----@return string icon ----@return string? name -local function get_folder_icon_webdev(node, has_children) - local icon, hl_group = M.devicons.get_icon(node.name, node.extension) - if not M.config.web_devicons.folder.color then - hl_group = nil - end - if icon ~= nil then - return icon, hl_group - else - return get_folder_icon_default(node, has_children) - end -end - ----@return string icon ----@return string? name -local function get_file_icon_default() - local hl_group = "NvimTreeFileIcon" - local icon = M.config.glyphs.default - if #icon > 0 then - return icon, hl_group - else - return "", nil - end -end - ----@param fname string ----@param extension string ----@return string icon ----@return string? name -local function get_file_icon_webdev(fname, extension) - local icon, hl_group = M.devicons.get_icon(fname, extension) - if not M.config.web_devicons.file.color then - hl_group = "NvimTreeFileIcon" - end - if icon and hl_group ~= "DevIconDefault" then - return icon, hl_group - elseif string.match(extension, "%.(.*)") then - -- If there are more extensions to the file, try to grab the icon for them recursively - return get_file_icon_webdev(fname, string.match(extension, "%.(.*)")) - else - local devicons_default = M.devicons.get_default_icon() - if devicons_default and type(devicons_default.icon) == "string" and type(devicons_default.name) == "string" then - return devicons_default.icon, "DevIcon" .. devicons_default.name - else - return get_file_icon_default() - end - end -end - -local function config_file_icon() - if M.config.show.file then - if M.devicons and M.config.web_devicons.file.enable then - M.get_file_icon = get_file_icon_webdev - else - M.get_file_icon = get_file_icon_default - end - else - M.get_file_icon = empty - end -end - -local function config_folder_icon() - if M.config.show.folder then - if M.devicons and M.config.web_devicons.folder.enable then - M.get_folder_icon = get_folder_icon_webdev - else - M.get_folder_icon = get_folder_icon_default - end - else - M.get_folder_icon = empty - end -end - -function M.reset_config() - config_symlinks() - config_file_icon() - config_folder_icon() -end - -function M.setup(opts) - M.config = opts.renderer.icons - - M.devicons = pcall(require, "nvim-web-devicons") and require("nvim-web-devicons") or nil -end - -return M diff --git a/lua/nvim-tree/renderer/components/init.lua b/lua/nvim-tree/renderer/components/init.lua index dfb767e63d8..776350b4b77 100644 --- a/lua/nvim-tree/renderer/components/init.lua +++ b/lua/nvim-tree/renderer/components/init.lua @@ -2,13 +2,13 @@ local M = {} M.diagnostics = require("nvim-tree.renderer.components.diagnostics") M.full_name = require("nvim-tree.renderer.components.full-name") -M.icons = require("nvim-tree.renderer.components.icons") +M.devicons = require("nvim-tree.renderer.components.devicons") M.padding = require("nvim-tree.renderer.components.padding") function M.setup(opts) M.diagnostics.setup(opts) M.full_name.setup(opts) - M.icons.setup(opts) + M.devicons.setup(opts) M.padding.setup(opts) end diff --git a/lua/nvim-tree/renderer/components/padding.lua b/lua/nvim-tree/renderer/components/padding.lua index fa34c23e13c..ccb550e3229 100644 --- a/lua/nvim-tree/renderer/components/padding.lua +++ b/lua/nvim-tree/renderer/components/padding.lua @@ -1,3 +1,5 @@ +local DirectoryNode = require("nvim-tree.node.directory") + local M = {} local function check_siblings_for_folder(node, with_arrows) @@ -62,7 +64,7 @@ end ---@param node Node ---@param markers table ---@param early_stop integer? ----@return HighlightedString[] +---@return HighlightedString function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_stop) local str = "" @@ -90,8 +92,9 @@ function M.get_arrows(node) local str local hl = "NvimTreeFolderArrowClosed" - if node.nodes then - if node.open then + local dir = node:as(DirectoryNode) + if dir then + if dir.open then str = M.config.icons.glyphs.folder["arrow_open"] .. " " hl = "NvimTreeFolderArrowOpen" else diff --git a/lua/nvim-tree/renderer/decorator/bookmarks.lua b/lua/nvim-tree/renderer/decorator/bookmarks.lua index 6b33970fe90..3c188721c0c 100644 --- a/lua/nvim-tree/renderer/decorator/bookmarks.lua +++ b/lua/nvim-tree/renderer/decorator/bookmarks.lua @@ -19,7 +19,7 @@ function DecoratorBookmarks:create(opts, explorer) hl_pos = HL_POSITION[opts.renderer.highlight_bookmarks] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.bookmarks_placement] or ICON_PLACEMENT.none, } - o = self:new(o) --[[@as DecoratorBookmarks]] + o = self:new(o) if opts.renderer.icons.show.bookmarks then o.icon = { diff --git a/lua/nvim-tree/renderer/decorator/copied.lua b/lua/nvim-tree/renderer/decorator/copied.lua index 0debcc632bb..3d760a97a55 100644 --- a/lua/nvim-tree/renderer/decorator/copied.lua +++ b/lua/nvim-tree/renderer/decorator/copied.lua @@ -19,7 +19,7 @@ function DecoratorCopied:create(opts, explorer) hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none, icon_placement = ICON_PLACEMENT.none, } - o = self:new(o) --[[@as DecoratorCopied]] + o = self:new(o) return o end diff --git a/lua/nvim-tree/renderer/decorator/cut.lua b/lua/nvim-tree/renderer/decorator/cut.lua index b81642f6e79..45428969e7e 100644 --- a/lua/nvim-tree/renderer/decorator/cut.lua +++ b/lua/nvim-tree/renderer/decorator/cut.lua @@ -18,7 +18,7 @@ function DecoratorCut:create(opts, explorer) hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none, icon_placement = ICON_PLACEMENT.none, } - o = self:new(o) --[[@as DecoratorCut]] + o = self:new(o) return o end diff --git a/lua/nvim-tree/renderer/decorator/diagnostics.lua b/lua/nvim-tree/renderer/decorator/diagnostics.lua index 3daee7bc03a..ee495ca674e 100644 --- a/lua/nvim-tree/renderer/decorator/diagnostics.lua +++ b/lua/nvim-tree/renderer/decorator/diagnostics.lua @@ -4,6 +4,7 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local Decorator = require("nvim-tree.renderer.decorator") +local DirectoryNode = require("nvim-tree.node.directory") -- highlight groups by severity local HG_ICON = { @@ -48,7 +49,7 @@ function DecoratorDiagnostics:create(opts, explorer) hl_pos = HL_POSITION[opts.renderer.highlight_diagnostics] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.diagnostics_placement] or ICON_PLACEMENT.none, } - o = self:new(o) --[[@as DecoratorDiagnostics]] + o = self:new(o) if not o.enabled then return o @@ -98,7 +99,7 @@ function DecoratorDiagnostics:calculate_highlight(node) end local group - if node.nodes then + if node:is(DirectoryNode) then group = HG_FOLDER[diag_value] else group = HG_FILE[diag_value] diff --git a/lua/nvim-tree/renderer/decorator/git.lua b/lua/nvim-tree/renderer/decorator/git.lua index af2c8ccaa94..4a543bcd30a 100644 --- a/lua/nvim-tree/renderer/decorator/git.lua +++ b/lua/nvim-tree/renderer/decorator/git.lua @@ -4,15 +4,22 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local Decorator = require("nvim-tree.renderer.decorator") +local DirectoryNode = require("nvim-tree.node.directory") ----@class HighlightedStringGit: HighlightedString +---@class (exact) GitHighlightedString: HighlightedString ---@field ord number decreasing priority +---@alias GitStatusStrings "deleted" | "ignored" | "renamed" | "staged" | "unmerged" | "unstaged" | "untracked" + +---@alias GitIconsByStatus table human status +---@alias GitIconsByXY table porcelain status +---@alias GitGlyphsByStatus table from opts + ---@class (exact) DecoratorGit: Decorator ----@field file_hl table? by porcelain status e.g. "AM" ----@field folder_hl table? by porcelain status ----@field icons_by_status HighlightedStringGit[]? by human status ----@field icons_by_xy table? by porcelain status +---@field file_hl_by_xy table? +---@field folder_hl_by_xy table? +---@field icons_by_status GitIconsByStatus? +---@field icons_by_xy GitIconsByXY? local DecoratorGit = Decorator:new() ---Static factory method @@ -27,14 +34,14 @@ function DecoratorGit:create(opts, explorer) hl_pos = HL_POSITION[opts.renderer.highlight_git] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.git_placement] or ICON_PLACEMENT.none, } - o = self:new(o) --[[@as DecoratorGit]] + o = self:new(o) if not o.enabled then return o end if o.hl_pos ~= HL_POSITION.none then - o:build_hl_table() + o:build_file_folder_hl_by_xy() end if opts.renderer.icons.show.git then @@ -49,20 +56,19 @@ function DecoratorGit:create(opts, explorer) return o end ----@param glyphs table user glyps +---@param glyphs GitGlyphsByStatus function DecoratorGit:build_icons_by_status(glyphs) - self.icons_by_status = { - staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 }, - unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 }, - renamed = { str = glyphs.renamed, hl = { "NvimTreeGitRenamedIcon" }, ord = 3 }, - deleted = { str = glyphs.deleted, hl = { "NvimTreeGitDeletedIcon" }, ord = 4 }, - unmerged = { str = glyphs.unmerged, hl = { "NvimTreeGitMergeIcon" }, ord = 5 }, - untracked = { str = glyphs.untracked, hl = { "NvimTreeGitNewIcon" }, ord = 6 }, - ignored = { str = glyphs.ignored, hl = { "NvimTreeGitIgnoredIcon" }, ord = 7 }, - } + self.icons_by_status = {} + self.icons_by_status.staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 } + self.icons_by_status.unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 } + self.icons_by_status.renamed = { str = glyphs.renamed, hl = { "NvimTreeGitRenamedIcon" }, ord = 3 } + self.icons_by_status.deleted = { str = glyphs.deleted, hl = { "NvimTreeGitDeletedIcon" }, ord = 4 } + self.icons_by_status.unmerged = { str = glyphs.unmerged, hl = { "NvimTreeGitMergeIcon" }, ord = 5 } + self.icons_by_status.untracked = { str = glyphs.untracked, hl = { "NvimTreeGitNewIcon" }, ord = 6 } + self.icons_by_status.ignored = { str = glyphs.ignored, hl = { "NvimTreeGitIgnoredIcon" }, ord = 7 } end ----@param icons HighlightedStringGit[] +---@param icons GitIconsByXY function DecoratorGit:build_icons_by_xy(icons) self.icons_by_xy = { ["M "] = { icons.staged }, @@ -100,8 +106,8 @@ function DecoratorGit:build_icons_by_xy(icons) } end -function DecoratorGit:build_hl_table() - self.file_hl = { +function DecoratorGit:build_file_folder_hl_by_xy() + self.file_hl_by_xy = { ["M "] = "NvimTreeGitFileStagedHL", ["C "] = "NvimTreeGitFileStagedHL", ["AA"] = "NvimTreeGitFileStagedHL", @@ -134,9 +140,9 @@ function DecoratorGit:build_hl_table() [" A"] = "none", } - self.folder_hl = {} - for k, v in pairs(self.file_hl) do - self.folder_hl[k] = v:gsub("File", "Folder") + self.folder_hl_by_xy = {} + for k, v in pairs(self.file_hl_by_xy) do + self.folder_hl_by_xy[k] = v:gsub("File", "Folder") end end @@ -148,19 +154,19 @@ function DecoratorGit:calculate_icons(node) return nil end - local git_status = node:get_git_status() - if git_status == nil then + local git_xy = node:get_git_xy() + if git_xy == nil then return nil end local inserted = {} local iconss = {} - for _, s in pairs(git_status) do + for _, s in pairs(git_xy) do local icons = self.icons_by_xy[s] if not icons then if self.hl_pos == HL_POSITION.none then - notify.warn(string.format("Unrecognized git state '%s'", git_status)) + notify.warn(string.format("Unrecognized git state '%s'", git_xy)) end return nil end @@ -209,15 +215,15 @@ function DecoratorGit:calculate_highlight(node) return nil end - local git_status = node:get_git_status() - if not git_status then + local git_xy = node:get_git_xy() + if not git_xy then return nil end - if node.nodes then - return self.folder_hl[git_status[1]] + if node:is(DirectoryNode) then + return self.folder_hl_by_xy[git_xy[1]] else - return self.file_hl[git_status[1]] + return self.file_hl_by_xy[git_xy[1]] end end diff --git a/lua/nvim-tree/renderer/decorator/hidden.lua b/lua/nvim-tree/renderer/decorator/hidden.lua index 1df68c48295..7c62f51af5f 100644 --- a/lua/nvim-tree/renderer/decorator/hidden.lua +++ b/lua/nvim-tree/renderer/decorator/hidden.lua @@ -1,6 +1,8 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT + local Decorator = require("nvim-tree.renderer.decorator") +local DirectoryNode = require("nvim-tree.node.directory") ---@class (exact) DecoratorHidden: Decorator ---@field icon HighlightedString? @@ -18,7 +20,7 @@ function DecoratorHidden:create(opts, explorer) hl_pos = HL_POSITION[opts.renderer.highlight_hidden] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.hidden_placement] or ICON_PLACEMENT.none, } - o = self:new(o) --[[@as DecoratorHidden]] + o = self:new(o) if opts.renderer.icons.show.hidden then o.icon = { @@ -48,7 +50,7 @@ function DecoratorHidden:calculate_highlight(node) return nil end - if node.nodes then + if node:is(DirectoryNode) then return "NvimTreeHiddenFolderHL" else return "NvimTreeHiddenFileHL" diff --git a/lua/nvim-tree/renderer/decorator/modified.lua b/lua/nvim-tree/renderer/decorator/modified.lua index 4665343f0ee..2126379cd14 100644 --- a/lua/nvim-tree/renderer/decorator/modified.lua +++ b/lua/nvim-tree/renderer/decorator/modified.lua @@ -4,6 +4,7 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local Decorator = require("nvim-tree.renderer.decorator") +local DirectoryNode = require("nvim-tree.node.directory") ---@class (exact) DecoratorModified: Decorator ---@field icon HighlightedString|nil @@ -21,7 +22,7 @@ function DecoratorModified:create(opts, explorer) hl_pos = HL_POSITION[opts.renderer.highlight_modified] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.modified_placement] or ICON_PLACEMENT.none, } - o = self:new(o) --[[@as DecoratorModified]] + o = self:new(o) if not o.enabled then return o @@ -55,7 +56,7 @@ function DecoratorModified:calculate_highlight(node) return nil end - if node.nodes then + if node:is(DirectoryNode) then return "NvimTreeModifiedFolderHL" else return "NvimTreeModifiedFileHL" diff --git a/lua/nvim-tree/renderer/decorator/opened.lua b/lua/nvim-tree/renderer/decorator/opened.lua index 6f2ad58bbfc..cb1843311c7 100644 --- a/lua/nvim-tree/renderer/decorator/opened.lua +++ b/lua/nvim-tree/renderer/decorator/opened.lua @@ -21,7 +21,7 @@ function DecoratorOpened:create(opts, explorer) hl_pos = HL_POSITION[opts.renderer.highlight_opened_files] or HL_POSITION.none, icon_placement = ICON_PLACEMENT.none, } - o = self:new(o) --[[@as DecoratorOpened]] + o = self:new(o) return o end diff --git a/lua/nvim-tree/renderer/init.lua b/lua/nvim-tree/renderer/init.lua index e222e709b30..f61c4be4e5f 100644 --- a/lua/nvim-tree/renderer/init.lua +++ b/lua/nvim-tree/renderer/init.lua @@ -2,8 +2,6 @@ local log = require("nvim-tree.log") local view = require("nvim-tree.view") local events = require("nvim-tree.events") -local icon_component = require("nvim-tree.renderer.components.icons") - local Builder = require("nvim-tree.renderer.builder") local SIGN_GROUP = "NvimTreeRendererSigns" @@ -16,7 +14,6 @@ local namespace_virtual_lines_id = vim.api.nvim_create_namespace("NvimTreeVirtua ---@field private __index? table ---@field private opts table user options ---@field private explorer Explorer ----@field private builder Builder local Renderer = {} ---@param opts table user options @@ -27,7 +24,6 @@ function Renderer:new(opts, explorer) local o = { opts = opts, explorer = explorer, - builder = Builder:new(opts, explorer), } setmetatable(o, self) @@ -109,7 +105,6 @@ function Renderer:draw() local profile = log.profile_start("draw") local cursor = vim.api.nvim_win_get_cursor(view.get_winnr() or 0) - icon_component.reset_config() local builder = Builder:new(self.opts, self.explorer):build() diff --git a/lua/nvim-tree/view.lua b/lua/nvim-tree/view.lua index 408584d1bbc..e76bf089058 100644 --- a/lua/nvim-tree/view.lua +++ b/lua/nvim-tree/view.lua @@ -150,12 +150,30 @@ end local function set_window_options_and_buffer() pcall(vim.api.nvim_command, "buffer " .. M.get_bufnr()) - local eventignore = vim.opt.eventignore:get() - vim.opt.eventignore = "all" - for k, v in pairs(M.View.winopts) do - vim.opt_local[k] = v + + if vim.fn.has("nvim-0.10") == 1 then + + local eventignore = vim.api.nvim_get_option_value("eventignore", {}) + vim.api.nvim_set_option_value("eventignore", "all", {}) + + for k, v in pairs(M.View.winopts) do + vim.api.nvim_set_option_value(k, v, { scope = "local" }) + end + + vim.api.nvim_set_option_value("eventignore", eventignore, {}) + + else + + local eventignore = vim.api.nvim_get_option("eventignore") ---@diagnostic disable-line: deprecated + vim.api.nvim_set_option("eventignore", "all") ---@diagnostic disable-line: deprecated + + for k, v in pairs(M.View.winopts) do + vim.api.nvim_win_set_option(0, k, v) ---@diagnostic disable-line: deprecated + end + + vim.api.nvim_set_option("eventignore", eventignore) ---@diagnostic disable-line: deprecated + end - vim.opt.eventignore = eventignore end ---@return table diff --git a/lua/nvim-tree/watcher.lua b/lua/nvim-tree/watcher.lua index cad762e3dde..0dfdbf0afde 100644 --- a/lua/nvim-tree/watcher.lua +++ b/lua/nvim-tree/watcher.lua @@ -2,21 +2,7 @@ local notify = require("nvim-tree.notify") local log = require("nvim-tree.log") local utils = require("nvim-tree.utils") -local M = { - config = {}, -} - ----@class Event -local Event = { - _events = {}, -} -Event.__index = Event - ----@class Watcher -local Watcher = { - _watchers = {}, -} -Watcher.__index = Watcher +local Class = require("nvim-tree.class") local FS_EVENT_FLAGS = { -- inotify or equivalent will be used; fallback to stat has not yet been implemented @@ -25,20 +11,40 @@ local FS_EVENT_FLAGS = { recursive = false, } +local M = { + config = {}, +} + +---@class (exact) Event: Class +---@field destroyed boolean +---@field private path string +---@field private fs_event uv.uv_fs_event_t? +---@field private listeners function[] +local Event = Class:new() + +---Registry of all events +---@type Event[] +local events = {} + +---Static factory method +---Creates and starts an Event ---@param path string ---@return Event|nil -function Event:new(path) - log.line("watcher", "Event:new '%s'", path) - - local e = setmetatable({ - _path = path, - _fs_event = nil, - _listeners = {}, - }, Event) - - if e:start() then - Event._events[path] = e - return e +function Event:create(path) + log.line("watcher", "Event:create '%s'", path) + + ---@type Event + local o = { + destroyed = false, + path = path, + fs_event = nil, + listeners = {}, + } + o = self:new(o) + + if o:start() then + events[path] = o + return o else return nil end @@ -46,21 +52,21 @@ end ---@return boolean function Event:start() - log.line("watcher", "Event:start '%s'", self._path) + log.line("watcher", "Event:start '%s'", self.path) local rc, _, name - self._fs_event, _, name = vim.loop.new_fs_event() - if not self._fs_event then - self._fs_event = nil - notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self._path, name)) + self.fs_event, _, name = vim.loop.new_fs_event() + if not self.fs_event then + self.fs_event = nil + notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self.path, name)) return false end local event_cb = vim.schedule_wrap(function(err, filename) if err then - log.line("watcher", "event_cb '%s' '%s' FAIL : %s", self._path, filename, err) - local message = string.format("File system watcher failed (%s) for path %s, halting watcher.", err, self._path) + log.line("watcher", "event_cb '%s' '%s' FAIL : %s", self.path, filename, err) + local message = string.format("File system watcher failed (%s) for path %s, halting watcher.", err, self.path) if err == "EPERM" and (utils.is_windows or utils.is_wsl) then -- on directory removal windows will cascade the filesystem events out of order log.line("watcher", message) @@ -69,19 +75,19 @@ function Event:start() self:destroy(message) end else - log.line("watcher", "event_cb '%s' '%s'", self._path, filename) - for _, listener in ipairs(self._listeners) do + log.line("watcher", "event_cb '%s' '%s'", self.path, filename) + for _, listener in ipairs(self.listeners) do listener(filename) end end end) - rc, _, name = self._fs_event:start(self._path, FS_EVENT_FLAGS, event_cb) + rc, _, name = self.fs_event:start(self.path, FS_EVENT_FLAGS, event_cb) if rc ~= 0 then if name == "EMFILE" then M.disable_watchers("fs.inotify.max_user_watches exceeded, see https://github.com/nvim-tree/nvim-tree.lua/wiki/Troubleshooting") else - notify.warn(string.format("Could not start the fs_event watcher for path %s : %s", self._path, name)) + notify.warn(string.format("Could not start the fs_event watcher for path %s : %s", self.path, name)) end return false end @@ -91,81 +97,105 @@ end ---@param listener function function Event:add(listener) - table.insert(self._listeners, listener) + table.insert(self.listeners, listener) end ---@param listener function function Event:remove(listener) - utils.array_remove(self._listeners, listener) - if #self._listeners == 0 then + utils.array_remove(self.listeners, listener) + if #self.listeners == 0 then self:destroy() end end ---@param message string|nil function Event:destroy(message) - log.line("watcher", "Event:destroy '%s'", self._path) + log.line("watcher", "Event:destroy '%s'", self.path) - if self._fs_event then + if self.fs_event then if message then notify.warn(message) end - local rc, _, name = self._fs_event:stop() + local rc, _, name = self.fs_event:stop() if rc ~= 0 then - notify.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self._path, name)) + notify.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self.path, name)) end - self._fs_event = nil + self.fs_event = nil end - Event._events[self._path] = nil - self.destroyed = true + events[self.path] = nil end +---Static factory method +---Creates and starts a Watcher +---@class (exact) Watcher: Class +---@field data table user data +---@field destroyed boolean +---@field private path string +---@field private callback fun(watcher: Watcher) +---@field private files string[]? +---@field private listener fun(filename: string)? +---@field private event Event +local Watcher = Class:new() + +---Registry of all watchers +---@type Watcher[] +local watchers = {} + +---Static factory method ---@param path string ---@param files string[]|nil ----@param callback function ----@param data table +---@param callback fun(watcher: Watcher) +---@param data table user data ---@return Watcher|nil -function Watcher:new(path, files, callback, data) - log.line("watcher", "Watcher:new '%s' %s", path, vim.inspect(files)) - - local w = setmetatable(data, Watcher) +function Watcher:create(path, files, callback, data) + log.line("watcher", "Watcher:create '%s' %s", path, vim.inspect(files)) - w._event = Event._events[path] or Event:new(path) - w._listener = nil - w._path = path - w._files = files - w._callback = callback - - if not w._event then + local event = events[path] or Event:create(path) + if not event then return nil end - w:start() + ---@type Watcher + local o = { + data = data, + destroyed = false, + path = path, + callback = callback, + files = files, + listener = nil, + event = event, + } + o = self:new(o) + + o:start() - table.insert(Watcher._watchers, w) + table.insert(watchers, o) - return w + return o end function Watcher:start() - self._listener = function(filename) - if not self._files or vim.tbl_contains(self._files, filename) then - self._callback(self) + self.listener = function(filename) + if not self.files or vim.tbl_contains(self.files, filename) then + self.callback(self) end end - self._event:add(self._listener) + self.event:add(self.listener) end function Watcher:destroy() - log.line("watcher", "Watcher:destroy '%s'", self._path) + log.line("watcher", "Watcher:destroy '%s'", self.path) - self._event:remove(self._listener) + self.event:remove(self.listener) - utils.array_remove(Watcher._watchers, self) + utils.array_remove( + watchers, + self + ) self.destroyed = true end @@ -183,11 +213,11 @@ end function M.purge_watchers() log.line("watcher", "purge_watchers") - for _, w in ipairs(utils.array_shallow_clone(Watcher._watchers)) do + for _, w in ipairs(utils.array_shallow_clone(watchers)) do w:destroy() end - for _, e in pairs(Event._events) do + for _, e in pairs(events) do e:destroy() end end