From e962e97cab7c5638c7c36f7c532cd51cbc71054a Mon Sep 17 00:00:00 2001 From: Alexander Courtis Date: Sun, 25 Aug 2024 12:32:09 +1000 Subject: [PATCH] refactor(#2830): multi instance marks (#2873) * refactor(#2830): multi instance marks * refactor(#2830): multi instance marks * refactor(#2830): multi instance marks --- doc/nvim-tree-lua.txt | 4 +- lua/nvim-tree.lua | 1 - lua/nvim-tree/api.lua | 36 +-- lua/nvim-tree/explorer/filters.lua | 2 +- lua/nvim-tree/explorer/init.lua | 6 +- lua/nvim-tree/marks/bulk-delete.lua | 59 ---- lua/nvim-tree/marks/bulk-move.lua | 67 ----- lua/nvim-tree/marks/bulk-trash.lua | 53 ---- lua/nvim-tree/marks/init.lua | 265 +++++++++++++++--- lua/nvim-tree/marks/navigation.lua | 111 -------- .../renderer/decorator/bookmarks.lua | 4 +- 11 files changed, 249 insertions(+), 359 deletions(-) delete mode 100644 lua/nvim-tree/marks/bulk-delete.lua delete mode 100644 lua/nvim-tree/marks/bulk-move.lua delete mode 100644 lua/nvim-tree/marks/bulk-trash.lua delete mode 100644 lua/nvim-tree/marks/navigation.lua diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index b89e947e..f8188211 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -2195,8 +2195,8 @@ marks.navigate.prev() *nvim-tree-api.marks.navigate.prev()* As per |nvim-tree-api.marks.navigate.next()| marks.navigate.select() *nvim-tree-api.marks.navigate.select()* - Prompts for selection of a marked node as per - |nvim-tree-api.marks.navigate.next()| + Prompts for selection of a marked node, sorted by absolute paths. + A folder will be focused, a file will be opened. ============================================================================== 6.8 API CONFIG *nvim-tree-api.config* diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 14baa105..2ed07238 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -840,7 +840,6 @@ function M.setup(conf) require("nvim-tree.view").setup(opts) require("nvim-tree.lib").setup(opts) require("nvim-tree.renderer").setup(opts) - require("nvim-tree.marks").setup(opts) require("nvim-tree.buffers").setup(opts) require("nvim-tree.help").setup(opts) require("nvim-tree.watcher").setup(opts) diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua index 89a6a2ef..eea7b0ac 100644 --- a/lua/nvim-tree/api.lua +++ b/lua/nvim-tree/api.lua @@ -6,10 +6,6 @@ local actions = require "nvim-tree.actions" local appearance_diagnostics = require "nvim-tree.appearance.diagnostics" local events = require "nvim-tree.events" local help = require "nvim-tree.help" -local marks_navigation = require "nvim-tree.marks.navigation" -local marks_bulk_delete = require "nvim-tree.marks.bulk-delete" -local marks_bulk_trash = require "nvim-tree.marks.bulk-trash" -local marks_bulk_move = require "nvim-tree.marks.bulk-move" local keymap = require "nvim-tree.keymap" local notify = require "nvim-tree.notify" @@ -76,18 +72,6 @@ local function wrap_node_or_nil(fn) end end ----Inject the explorer as the first argument if present otherwise do nothing. ----@param fn function function to invoke ----@return fun(...) : any -local function wrap_explorer(fn) - return function(...) - local explorer = core.get_explorer() - if explorer then - return fn(explorer, ...) - end - end -end - ---Invoke a member's method on the singleton explorer. ---Print error when setup not called. ---@param explorer_member string explorer member name @@ -267,16 +251,16 @@ Api.events.Event = events.Event Api.live_filter.start = wrap_explorer_member("live_filter", "start_filtering") Api.live_filter.clear = wrap_explorer_member("live_filter", "clear_filter") -Api.marks.get = wrap_node(wrap_explorer_member("marks", "get_mark")) -Api.marks.list = wrap_explorer_member("marks", "get_marks") -Api.marks.toggle = wrap_node(wrap_explorer_member("marks", "toggle_mark")) -Api.marks.clear = wrap_explorer_member("marks", "clear_marks") -Api.marks.bulk.delete = wrap_explorer(marks_bulk_delete.bulk_delete) -Api.marks.bulk.trash = wrap_explorer(marks_bulk_trash.bulk_trash) -Api.marks.bulk.move = wrap_explorer(marks_bulk_move.bulk_move) -Api.marks.navigate.next = wrap(marks_navigation.next) -Api.marks.navigate.prev = wrap(marks_navigation.prev) -Api.marks.navigate.select = wrap(marks_navigation.select) +Api.marks.get = wrap_node(wrap_explorer_member("marks", "get")) +Api.marks.list = wrap_explorer_member("marks", "list") +Api.marks.toggle = wrap_node(wrap_explorer_member("marks", "toggle")) +Api.marks.clear = wrap_explorer_member("marks", "clear") +Api.marks.bulk.delete = wrap_explorer_member("marks", "bulk_delete") +Api.marks.bulk.trash = wrap_explorer_member("marks", "bulk_trash") +Api.marks.bulk.move = wrap_explorer_member("marks", "bulk_move") +Api.marks.navigate.next = wrap_explorer_member("marks", "navigate_next") +Api.marks.navigate.prev = wrap_explorer_member("marks", "navigate_prev") +Api.marks.navigate.select = wrap_explorer_member("marks", "navigate_select") Api.config.mappings.get_keymap = wrap(keymap.get_keymap) Api.config.mappings.get_keymap_default = wrap(keymap.get_keymap_default) diff --git a/lua/nvim-tree/explorer/filters.lua b/lua/nvim-tree/explorer/filters.lua index 6f2cd6df..e6e4cc8f 100644 --- a/lua/nvim-tree/explorer/filters.lua +++ b/lua/nvim-tree/explorer/filters.lua @@ -194,7 +194,7 @@ function Filters:prepare(git_status) local explorer = require("nvim-tree.core").get_explorer() if explorer then - for _, node in pairs(explorer.marks:get_marks()) do + for _, node in pairs(explorer.marks:list()) do status.bookmarks[node.absolute_path] = node.type end end diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua index f260bd17..95cbb1de 100644 --- a/lua/nvim-tree/explorer/init.lua +++ b/lua/nvim-tree/explorer/init.lua @@ -3,7 +3,7 @@ local notify = require "nvim-tree.notify" local watch = require "nvim-tree.explorer.watch" local explorer_node = require "nvim-tree.explorer.node" local Filters = require "nvim-tree.explorer.filters" -local Marks = require "nvim-tree.marks" +local Marks = {} -- circular dependencies local LiveFilter = require "nvim-tree.explorer.live-filter" local Sorters = require "nvim-tree.explorer.sorters" @@ -44,12 +44,12 @@ function Explorer.new(path) absolute_path = path, nodes = {}, open = true, - marks = Marks:new(), sorters = Sorters:new(M.config), }, Explorer) explorer.watcher = watch.create_watcher(explorer) explorer.filters = Filters:new(M.config, explorer) explorer.live_filter = LiveFilter:new(M.config, explorer) + explorer.marks = Marks:new(M.config, explorer) explorer:_load(explorer) return explorer end @@ -85,6 +85,8 @@ function M.setup(opts) require("nvim-tree.explorer.explore").setup(opts) require("nvim-tree.explorer.reload").setup(opts) require("nvim-tree.explorer.watch").setup(opts) + + Marks = require "nvim-tree.marks" end M.Explorer = Explorer diff --git a/lua/nvim-tree/marks/bulk-delete.lua b/lua/nvim-tree/marks/bulk-delete.lua deleted file mode 100644 index c09bb423..00000000 --- a/lua/nvim-tree/marks/bulk-delete.lua +++ /dev/null @@ -1,59 +0,0 @@ -local utils = require "nvim-tree.utils" -local remove_file = require "nvim-tree.actions.fs.remove-file" -local notify = require "nvim-tree.notify" -local lib = require "nvim-tree.lib" - -local M = { - config = {}, -} - ---- Delete nodes; each removal will be optionally notified ----@param nodes Node[] ----@param marks Marks -local function do_delete(marks, nodes) - for _, node in pairs(nodes) do - remove_file.remove(node) - end - - marks:clear_marks() - - if not M.config.filesystem_watchers.enable then - require("nvim-tree.actions.reloaders").reload_explorer() - end -end - ---- Delete marked nodes, optionally prompting ----@param explorer Explorer -function M.bulk_delete(explorer) - if not explorer then - return - end - - local marks = explorer.marks - - local nodes = marks:get_marks() - if not nodes or #nodes == 0 then - notify.warn "No bookmarksed to delete." - return - end - - if M.config.ui.confirm.remove then - local prompt_select = "Remove bookmarked ?" - local prompt_input = prompt_select .. " y/N: " - lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_delete", function(item_short) - utils.clear_prompt() - if item_short == "y" then - do_delete(marks, nodes) - end - end) - else - do_delete(marks, nodes) - end -end - -function M.setup(opts) - M.config.ui = opts.ui - M.config.filesystem_watchers = opts.filesystem_watchers -end - -return M diff --git a/lua/nvim-tree/marks/bulk-move.lua b/lua/nvim-tree/marks/bulk-move.lua deleted file mode 100644 index 44c30c43..00000000 --- a/lua/nvim-tree/marks/bulk-move.lua +++ /dev/null @@ -1,67 +0,0 @@ -local core = require "nvim-tree.core" -local utils = require "nvim-tree.utils" -local rename_file = require "nvim-tree.actions.fs.rename-file" -local notify = require "nvim-tree.notify" -local lib = require "nvim-tree.lib" - -local M = { - config = {}, -} - ----@param explorer Explorer -function M.bulk_move(explorer) - if not explorer then - return - end - local marks = explorer.marks - - if #marks:get_marks() == 0 then - notify.warn "No bookmarks to move." - return - end - - local node_at_cursor = lib.get_node_at_cursor() - local default_path = core.get_cwd() - - if node_at_cursor and node_at_cursor.type == "directory" then - default_path = node_at_cursor.absolute_path - elseif node_at_cursor and node_at_cursor.parent then - default_path = node_at_cursor.parent.absolute_path - end - - local input_opts = { - prompt = "Move to: ", - default = default_path, - completion = "dir", - } - - vim.ui.input(input_opts, function(location) - utils.clear_prompt() - if not location or location == "" then - return - end - if vim.fn.filewritable(location) ~= 2 then - notify.warn(location .. " is not writable, cannot move.") - return - end - - local nodes = marks:get_marks() - for _, node in pairs(nodes) do - local head = vim.fn.fnamemodify(node.absolute_path, ":t") - local to = utils.path_join { location, head } - rename_file.rename(node, to) - end - - marks:clear_marks() - - if not M.config.filesystem_watchers.enable then - require("nvim-tree.actions.reloaders").reload_explorer() - end - end) -end - -function M.setup(opts) - M.config.filesystem_watchers = opts.filesystem_watchers -end - -return M diff --git a/lua/nvim-tree/marks/bulk-trash.lua b/lua/nvim-tree/marks/bulk-trash.lua deleted file mode 100644 index 7e792324..00000000 --- a/lua/nvim-tree/marks/bulk-trash.lua +++ /dev/null @@ -1,53 +0,0 @@ -local utils = require "nvim-tree.utils" -local remove_file = require "nvim-tree.actions.fs.trash" -local notify = require "nvim-tree.notify" -local lib = require "nvim-tree.lib" - -local M = { - config = {}, -} - ---- Delete nodes; each removal will be optionally notified ----@param nodes Node[] -local function do_trash(nodes) - for _, node in pairs(nodes) do - remove_file.remove(node) - end -end - ----@param explorer Explorer -function M.bulk_trash(explorer) - if not explorer then - return - end - - local marks = explorer.marks - - local nodes = marks:get_marks() - if not nodes or #nodes == 0 then - notify.warn "No bookmarks to trash." - return - end - - if M.config.ui.confirm.trash then - local prompt_select = "Trash bookmarked ?" - local prompt_input = prompt_select .. " y/N: " - lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_trash", function(item_short) - utils.clear_prompt() - if item_short == "y" then - do_trash(nodes) - marks:clear_marks() - end - end) - else - do_trash(nodes) - marks:clear_marks() - end -end - -function M.setup(opts) - M.config.ui = opts.ui - M.config.filesystem_watchers = opts.filesystem_watchers -end - -return M diff --git a/lua/nvim-tree/marks/init.lua b/lua/nvim-tree/marks/init.lua index c5ab5291..8947dfa6 100644 --- a/lua/nvim-tree/marks/init.lua +++ b/lua/nvim-tree/marks/init.lua @@ -1,65 +1,82 @@ -local renderer = {} -- circular dependency +local Iterator = require "nvim-tree.iterators.node-iterator" +local core = require "nvim-tree.core" +local lib = require "nvim-tree.lib" +local notify = require "nvim-tree.notify" +local open_file = require "nvim-tree.actions.node.open-file" +local remove_file = require "nvim-tree.actions.fs.remove-file" +local rename_file = require "nvim-tree.actions.fs.rename-file" +local renderer = require "nvim-tree.renderer" +local trash = require "nvim-tree.actions.fs.trash" +local utils = require "nvim-tree.utils" ---@class Marks ----@field private marks Node[] +---@field config table hydrated user opts.filters +---@field private explorer Explorer +---@field private marks table by absolute path local Marks = {} ---@return Marks -function Marks:new() - local o = {} +---@param opts table user options +---@param explorer Explorer +function Marks:new(opts, explorer) + local o = { + explorer = explorer, + config = { + ui = opts.ui, + filesystem_watchers = opts.filesystem_watchers, + }, + marks = {}, + } + setmetatable(o, self) self.__index = self - - o.marks = {} - return o end +---Clear all marks and reload if watchers disabled ---@private ----@param node Node -function Marks:add_mark(node) - self.marks[node.absolute_path] = node +function Marks:clear_reload() + self:clear() + if not self.config.filesystem_watchers.enable then + require("nvim-tree.actions.reloaders").reload_explorer() + end +end +---Clear all marks and redraw +---@public +function Marks:clear() + self.marks = {} renderer.draw() end ----@private +---@public ---@param node Node -function Marks:remove_mark(node) - self.marks[node.absolute_path] = nil - - renderer.draw() -end - ----@param node Node -function Marks:toggle_mark(node) +function Marks:toggle(node) if node.absolute_path == nil then return end - if self:get_mark(node) then - self:remove_mark(node) + if self:get(node) then + self.marks[node.absolute_path] = nil else - self:add_mark(node) + self.marks[node.absolute_path] = node end renderer.draw() end -function Marks:clear_marks() - self.marks = {} - - renderer.draw() -end - +---Return node if marked +---@public ---@param node Node ---@return Node|nil -function Marks:get_mark(node) +function Marks:get(node) return node and self.marks[node.absolute_path] end +---List marked nodes +---@public ---@return Node[] -function Marks:get_marks() +function Marks:list() local list = {} for _, node in pairs(self.marks) do table.insert(list, node) @@ -67,12 +84,190 @@ function Marks:get_marks() return list end -function Marks.setup(opts) - renderer = require "nvim-tree.renderer" +---Delete marked; each removal will be optionally notified +---@public +function Marks:bulk_delete() + if not next(self.marks) then + notify.warn "No bookmarks to delete." + return + end - require("nvim-tree.marks.bulk-delete").setup(opts) - require("nvim-tree.marks.bulk-trash").setup(opts) - require("nvim-tree.marks.bulk-move").setup(opts) + local function execute() + for _, node in pairs(self.marks) do + remove_file.remove(node) + end + self:clear_reload() + end + + if self.config.ui.confirm.remove then + local prompt_select = "Remove bookmarked ?" + local prompt_input = prompt_select .. " y/N: " + lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_delete", function(item_short) + utils.clear_prompt() + if item_short == "y" then + execute() + end + end) + else + execute() + end +end + +---Trash marked; each removal will be optionally notified +---@public +function Marks:bulk_trash() + if not next(self.marks) then + notify.warn "No bookmarks to trash." + return + end + + local function execute() + for _, node in pairs(self.marks) do + trash.remove(node) + end + self:clear_reload() + end + + if self.config.ui.confirm.trash then + local prompt_select = "Trash bookmarked ?" + local prompt_input = prompt_select .. " y/N: " + lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_trash", function(item_short) + utils.clear_prompt() + if item_short == "y" then + execute() + end + end) + else + execute() + end +end + +---Move marked +---@public +function Marks:bulk_move() + if not next(self.marks) then + notify.warn "No bookmarks to move." + return + end + + local node_at_cursor = lib.get_node_at_cursor() + local default_path = core.get_cwd() + + if node_at_cursor and node_at_cursor.type == "directory" then + default_path = node_at_cursor.absolute_path + elseif node_at_cursor and node_at_cursor.parent then + default_path = node_at_cursor.parent.absolute_path + end + + local input_opts = { + prompt = "Move to: ", + default = default_path, + completion = "dir", + } + + vim.ui.input(input_opts, function(location) + utils.clear_prompt() + if not location or location == "" then + return + end + if vim.fn.filewritable(location) ~= 2 then + notify.warn(location .. " is not writable, cannot move.") + return + end + + for _, node in pairs(self.marks) do + local head = vim.fn.fnamemodify(node.absolute_path, ":t") + local to = utils.path_join { location, head } + rename_file.rename(node, to) + end + + self:clear_reload() + end) +end + +---Focus nearest marked node in direction. +---@private +---@param up boolean +function Marks:navigate(up) + local node = lib.get_node_at_cursor() + if not node then + return + end + + local first, prev, next, last = nil, nil, nil, nil + local found = false + + Iterator.builder(self.explorer.nodes) + :recursor(function(n) + return n.open and n.nodes + end) + :applier(function(n) + if n.absolute_path == node.absolute_path then + found = true + return + end + + if not self:get(n) then + return + end + + last = n + first = first or n + + if found and not next then + next = n + end + + if not found then + prev = n + end + end) + :iterate() + + if not found then + return + end + + if up then + utils.focus_node_or_parent(prev or last) + else + utils.focus_node_or_parent(next or first) + end +end + +---@public +function Marks:navigate_prev() + self:navigate(true) +end + +---@public +function Marks:navigate_next() + self:navigate(false) +end + +---Prompts for selection of a marked node, sorted by absolute paths. +---A folder will be focused, a file will be opened. +---@public +function Marks:navigate_select() + local list = vim.tbl_map(function(n) + return n.absolute_path + end, self:list()) + + table.sort(list) + + vim.ui.select(list, { + prompt = "Select a file to open or a folder to focus", + }, function(choice) + if not choice or choice == "" then + return + end + local node = self:get { absolute_path = choice } + if node and not node.nodes 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) + end + end) end return Marks diff --git a/lua/nvim-tree/marks/navigation.lua b/lua/nvim-tree/marks/navigation.lua deleted file mode 100644 index a960cee6..00000000 --- a/lua/nvim-tree/marks/navigation.lua +++ /dev/null @@ -1,111 +0,0 @@ -local Iterator = require "nvim-tree.iterators.node-iterator" -local core = require "nvim-tree.core" -local open_file = require "nvim-tree.actions.node.open-file" -local utils = require "nvim-tree.utils" -local lib = require "nvim-tree.lib" - ----@param node table ----@param where string ----@return Node|nil -local function get_nearest(node, where) - local explorer = core.get_explorer() - if not explorer then - return - end - - local first, prev, next, last = nil, nil, nil, nil - local found = false - - Iterator.builder(explorer.nodes) - :recursor(function(n) - return n.open and n.nodes - end) - :applier(function(n) - if n.absolute_path == node.absolute_path then - found = true - return - end - - if not explorer.marks:get_mark(n) then - return - end - - last = n - first = first or n - - if found and not next then - next = n - end - - if not found then - prev = n - end - end) - :iterate() - - if not found then - return - end - - if where == "next" then - return next or first - else - return prev or last - end -end - ----@param where string ----@param node table|nil ----@return Node|nil -local function get(where, node) - if node then - return get_nearest(node, where) - end -end - ----@param node table|nil -local function open_or_focus(node) - if node and not node.nodes 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) - end -end - ----@param where string ----@return function -local function navigate_to(where) - return function() - local node = lib.get_node_at_cursor() - local next = get(where, node) - open_or_focus(next) - end -end - -local M = {} - -M.next = navigate_to "next" -M.prev = navigate_to "prev" - -function M.select() - local explorer = core.get_explorer() - if not explorer then - return - end - - local list = vim.tbl_map(function(n) - return n.absolute_path - end, explorer.marks:get_marks()) - - vim.ui.select(list, { - prompt = "Select a file to open or a folder to focus", - }, function(choice) - if not choice or choice == "" then - return - end - local node = explorer.marks:get_mark { absolute_path = choice } - open_or_focus(node) - end) -end - -return M diff --git a/lua/nvim-tree/renderer/decorator/bookmarks.lua b/lua/nvim-tree/renderer/decorator/bookmarks.lua index 700ee8e7..56065fd7 100644 --- a/lua/nvim-tree/renderer/decorator/bookmarks.lua +++ b/lua/nvim-tree/renderer/decorator/bookmarks.lua @@ -34,7 +34,7 @@ end ---@param node Node ---@return HighlightedString[]|nil icons function DecoratorBookmarks:calculate_icons(node) - if core.get_explorer() and core.get_explorer().marks:get_mark(node) then + if core.get_explorer() and core.get_explorer().marks:get(node) then return { self.icon } end end @@ -43,7 +43,7 @@ end ---@param node Node ---@return string|nil group function DecoratorBookmarks:calculate_highlight(node) - if self.hl_pos ~= HL_POSITION.none and core.get_explorer() and core.get_explorer().marks:get_mark(node) then + if self.hl_pos ~= HL_POSITION.none and core.get_explorer() and core.get_explorer().marks:get(node) then return "NvimTreeBookmarkHL" end end