diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index 6aadaf3dc27..a89f9ed3415 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -32,8 +32,9 @@ CONTENTS *nvim-tree* 5.16 Opts: Notify |nvim-tree-opts-notify| 5.17 Opts: Help |nvim-tree-opts-help| 5.18 Opts: UI |nvim-tree-opts-ui| - 5.19 Opts: Experimental |nvim-tree-opts-experimental| - 5.20 Opts: Log |nvim-tree-opts-log| + 5.19 Opts: Bookmarks |nvim-tree-opts-bookmarks| + 5.20 Opts: Experimental |nvim-tree-opts-experimental| + 5.21 Opts: Log |nvim-tree-opts-log| 6. API |nvim-tree-api| 6.1 API Tree |nvim-tree-api.tree| 6.2 API File System |nvim-tree-api.fs| @@ -639,6 +640,9 @@ Following is the default configuration. See |nvim-tree-opts| for details. >lua default_yes = false, }, }, + bookmarks = { + persist = false, + }, experimental = { }, log = { @@ -1657,14 +1661,24 @@ Confirmation prompts. Type: `boolean`, Default: `false` ============================================================================== - 5.19 OPTS: EXPERIMENTAL *nvim-tree-opts-experimental* + 5.19 OPTS: BOOKMARKS *nvim-tree-opts-bookmarks* + +*nvim-tree.bookmarks.persist* +Persist bookmarks to a json file containing a list of absolute paths. +Type: `boolean` | `string`, Default: `false` + +`true`: use default: `stdpath("data") .. "/nvim-tree-bookmarks.json"` +`string`: absolute path of your choice. + +============================================================================== + 5.20 OPTS: EXPERIMENTAL *nvim-tree-opts-experimental* *nvim-tree.experimental* Experimental features that may become default or optional functionality. In the event of a problem please disable the experiment and raise an issue. ============================================================================== - 5.20 OPTS: LOG *nvim-tree-opts-log* + 5.21 OPTS: LOG *nvim-tree-opts-log* Configuration for diagnostic logging. @@ -3189,6 +3203,7 @@ highlight group is not, hard linking as follows: > |nvim-tree.actions.remove_file.close_window| |nvim-tree.actions.use_system_clipboard| |nvim-tree.auto_reload_on_write| +|nvim-tree.bookmarks.persist| |nvim-tree.diagnostics.debounce_delay| |nvim-tree.diagnostics.diagnostic_opts| |nvim-tree.diagnostics.enable| diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index dc0178b4a18..9fe579ccdfe 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -514,6 +514,9 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS default_yes = false, }, }, + bookmarks = { + persist = false, + }, experimental = { }, log = { @@ -530,7 +533,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS watcher = false, }, }, -} -- END_DEFAULT_OPTS +}-- END_DEFAULT_OPTS local function merge_options(conf) return vim.tbl_deep_extend("force", DEFAULT_OPTS, conf or {}) @@ -581,6 +584,9 @@ local ACCEPTED_TYPES = { }, }, }, + bookmarks = { + persist = { "boolean", "string" }, + }, } local ACCEPTED_STRINGS = { diff --git a/lua/nvim-tree/explorer/filters.lua b/lua/nvim-tree/explorer/filters.lua index fda0a79fbaf..e26739939dc 100644 --- a/lua/nvim-tree/explorer/filters.lua +++ b/lua/nvim-tree/explorer/filters.lua @@ -208,8 +208,8 @@ function Filters:prepare(project) local explorer = require("nvim-tree.core").get_explorer() if explorer then - for _, node in pairs(explorer.marks:list()) do - status.bookmarks[node.absolute_path] = node.type + for _, node in ipairs(explorer.marks:list()) do + status.bookmarks[node.absolute_path] = node end end diff --git a/lua/nvim-tree/marks/init.lua b/lua/nvim-tree/marks/init.lua index 22ac572dee0..739c35e92ce 100644 --- a/lua/nvim-tree/marks/init.lua +++ b/lua/nvim-tree/marks/init.lua @@ -11,6 +11,53 @@ local utils = require("nvim-tree.utils") local Class = require("nvim-tree.classic") local DirectoryNode = require("nvim-tree.node.directory") +local function get_save_path(opts) + if type(opts.bookmarks.persist) == "string" then + return opts.bookmarks.persist + else + return vim.fn.stdpath("data") .. "/nvim-tree-bookmarks.json" + end +end + +local function save_bookmarks(marks, opts) + if not opts.bookmarks.persist then + return + end + + local storepath = get_save_path(opts) + local file, errmsg = io.open(storepath, "w") + if file then + local data = {} + for path, _ in pairs(marks) do + table.insert(data, path) + end + file:write(vim.json.encode(data)) + file:close() + else + notify.warn("Invalid bookmarks.save_path, disabling persistence: " .. errmsg) + opts.bookmarks.persist = false + end +end + +local function load_bookmarks(opts) + local storepath = get_save_path(opts) + local file = io.open(storepath, "r") + if file then + local content = file:read("*all") + file:close() + if content and content ~= "" then + local data = vim.json.decode(content) + local marks = {} + for _, path in ipairs(data) do + -- Store as boolean initially; will be lazily resolved to node on first access + marks[path] = true + end + return marks + end + end + return {} +end + ---@class (exact) Marks: Class ---@field private explorer Explorer ---@field private marks table by absolute path @@ -26,8 +73,15 @@ local Marks = Class:extend() ---@param args MarksArgs function Marks:new(args) self.explorer = args.explorer - self.marks = {} + if self.explorer.opts.bookmarks.persist then + local ok, loaded_marks = pcall(load_bookmarks, self.explorer.opts) + if ok then + self.marks = loaded_marks + else + notify.warn("Failed to load bookmarks: " .. loaded_marks) + end + end end ---Clear all marks and reload if watchers disabled @@ -59,6 +113,12 @@ function Marks:toggle(node) self.marks[node.absolute_path] = node end + if self.explorer.opts.bookmarks.persist then + local ok, err = pcall(save_bookmarks, self.marks, self.explorer.opts) + if not ok then + notify.warn("Failed to save bookmarks: " .. err) + end + end self.explorer.renderer:draw() end @@ -67,7 +127,21 @@ end ---@param node Node ---@return Node|nil function Marks:get(node) - return node and self.marks[node.absolute_path] + if not node or not node.absolute_path then + return nil + end + local mark = self.marks[node.absolute_path] + if mark == true then + -- Lazy resolve: try to find node in explorer tree + local resolved_node = self.explorer:get_node_from_path(node.absolute_path) + if resolved_node then + -- Cache the resolved node + self.marks[node.absolute_path] = resolved_node + return resolved_node + end + return nil + end + return mark end ---List marked nodes @@ -75,8 +149,23 @@ end ---@return Node[] function Marks:list() local list = {} - for _, node in pairs(self.marks) do - table.insert(list, node) + for path, mark in pairs(self.marks) do + local node + if mark == true then + -- Lazy resolve: try to find node in explorer tree + node = self.explorer:get_node_from_path(path) + if node then + -- Cache the resolved node for future access + self.marks[path] = node + end + -- If node not found (file deleted/moved), skip it silently + else + -- Already a node object + node = mark + end + if node then + table.insert(list, node) + end end return list end @@ -90,7 +179,7 @@ function Marks:bulk_delete() end local function execute() - for _, node in pairs(self.marks) do + for _, node in ipairs(self:list()) do remove_file.remove(node) end self:clear_reload() @@ -119,7 +208,7 @@ function Marks:bulk_trash() end local function execute() - for _, node in pairs(self.marks) do + for _, node in ipairs(self:list()) do trash.remove(node) end self:clear_reload() @@ -172,7 +261,7 @@ function Marks:bulk_move() return end - for _, node in pairs(self.marks) do + for _, node in ipairs(self:list()) do local head = vim.fn.fnamemodify(node.absolute_path, ":t") local to = utils.path_join({ location, head }) rename_file.rename(node, to) @@ -259,7 +348,17 @@ function Marks:navigate_select() if not choice or choice == "" then return end - local node = self.marks[choice] + local mark = self.marks[choice] + local node + if mark == true then + -- Lazy resolve + node = self.explorer:get_node_from_path(choice) + if node then + self.marks[choice] = node + end + else + node = mark + end 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