Skip to content

Commit c89215d

Browse files
feat(#1851): add bookmarks.persist, default path: vim.fn.stdpath("data") .. "/nvim-tree-bookmarks.json", disabled by default (#3033)
* Support Persist Bookmarks * Fix node new open error * Fix filter marks index issue * Made saving/loading conditional on enable_persistence, * add method comments * Use a pcall to prevent an error being raised * Print errmsg and disable persistence on a bad path Co-authored-by: Alexander Courtis <alex@courtis.org> * fix: use public list() method instead of accessing private marks field * refactor: rename marks config to bookmarks and simplify persistence API - Rename opts.marks to opts.bookmarks for better clarity - Simplify persist option to boolean | string (true uses default path) - Implement lazy node resolution for loaded bookmarks - Update documentation with new bookmarks configuration - Improve bulk operations to use public list() method consistently * refactor: rename marks config to bookmarks and simplify persistence API - Rename opts.marks to opts.bookmarks for better clarity - Simplify persist option to boolean | string (true uses default path) - Implement lazy node resolution for loaded bookmarks - Update documentation with new bookmarks configuration - Improve bulk operations to use public list() method consistently * add errmsg in return Co-authored-by: Alexander Courtis <alex@courtis.org> * fix: correct indentation in filters.lua to match codestyle * fix: add missing else block to handle bookmark save errors and use errmsg variable * refactor: use string.format for bookmark warnings and update documentation - Replace string concatenation with string.format in bookmark error messages - Remove outdated comment about bookmark persistence from documentation --------- Co-authored-by: Alexander Courtis <alex@courtis.org>
1 parent e66994d commit c89215d

File tree

4 files changed

+135
-16
lines changed

4 files changed

+135
-16
lines changed

doc/nvim-tree-lua.txt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ CONTENTS *nvim-tree*
3232
5.16 Opts: Notify |nvim-tree-opts-notify|
3333
5.17 Opts: Help |nvim-tree-opts-help|
3434
5.18 Opts: UI |nvim-tree-opts-ui|
35-
5.19 Opts: Experimental |nvim-tree-opts-experimental|
36-
5.20 Opts: Log |nvim-tree-opts-log|
35+
5.19 Opts: Bookmarks |nvim-tree-opts-bookmarks|
36+
5.20 Opts: Experimental |nvim-tree-opts-experimental|
37+
5.21 Opts: Log |nvim-tree-opts-log|
3738
6. API |nvim-tree-api|
3839
6.1 API Tree |nvim-tree-api.tree|
3940
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
639640
default_yes = false,
640641
},
641642
},
643+
bookmarks = {
644+
persist = false,
645+
},
642646
experimental = {
643647
},
644648
log = {
@@ -1401,7 +1405,6 @@ delete/wipe. A reload or filesystem event will result in an update.
14011405
*nvim-tree.filters.no_bookmark*
14021406
Do not show files that are not bookmarked.
14031407
Toggle via |nvim-tree-api.tree.toggle_no_bookmark_filter()|, default `M`
1404-
Enabling this is not useful as there is no means yet to persist bookmarks.
14051408
Type: `boolean`, Default: `false`
14061409

14071410
*nvim-tree.filters.custom*
@@ -1657,14 +1660,24 @@ Confirmation prompts.
16571660
Type: `boolean`, Default: `false`
16581661

16591662
==============================================================================
1660-
5.19 OPTS: EXPERIMENTAL *nvim-tree-opts-experimental*
1663+
5.19 OPTS: BOOKMARKS *nvim-tree-opts-bookmarks*
1664+
1665+
*nvim-tree.bookmarks.persist*
1666+
Persist bookmarks to a json file containing a list of absolute paths.
1667+
Type: `boolean` | `string`, Default: `false`
1668+
1669+
`true`: use default: `stdpath("data") .. "/nvim-tree-bookmarks.json"`
1670+
`string`: absolute path of your choice.
1671+
1672+
==============================================================================
1673+
5.20 OPTS: EXPERIMENTAL *nvim-tree-opts-experimental*
16611674

16621675
*nvim-tree.experimental*
16631676
Experimental features that may become default or optional functionality.
16641677
In the event of a problem please disable the experiment and raise an issue.
16651678

16661679
==============================================================================
1667-
5.20 OPTS: LOG *nvim-tree-opts-log*
1680+
5.21 OPTS: LOG *nvim-tree-opts-log*
16681681

16691682
Configuration for diagnostic logging.
16701683

@@ -3189,6 +3202,7 @@ highlight group is not, hard linking as follows: >
31893202
|nvim-tree.actions.remove_file.close_window|
31903203
|nvim-tree.actions.use_system_clipboard|
31913204
|nvim-tree.auto_reload_on_write|
3205+
|nvim-tree.bookmarks.persist|
31923206
|nvim-tree.diagnostics.debounce_delay|
31933207
|nvim-tree.diagnostics.diagnostic_opts|
31943208
|nvim-tree.diagnostics.enable|

lua/nvim-tree.lua

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,9 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
514514
default_yes = false,
515515
},
516516
},
517+
bookmarks = {
518+
persist = false,
519+
},
517520
experimental = {
518521
},
519522
log = {
@@ -530,7 +533,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
530533
watcher = false,
531534
},
532535
},
533-
} -- END_DEFAULT_OPTS
536+
}-- END_DEFAULT_OPTS
534537

535538
local function merge_options(conf)
536539
return vim.tbl_deep_extend("force", DEFAULT_OPTS, conf or {})
@@ -581,6 +584,9 @@ local ACCEPTED_TYPES = {
581584
},
582585
},
583586
},
587+
bookmarks = {
588+
persist = { "boolean", "string" },
589+
},
584590
}
585591

586592
local ACCEPTED_STRINGS = {

lua/nvim-tree/explorer/filters.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ function Filters:prepare(project)
208208

209209
local explorer = require("nvim-tree.core").get_explorer()
210210
if explorer then
211-
for _, node in pairs(explorer.marks:list()) do
212-
status.bookmarks[node.absolute_path] = node.type
211+
for _, node in ipairs(explorer.marks:list()) do
212+
status.bookmarks[node.absolute_path] = node
213213
end
214214
end
215215

lua/nvim-tree/marks/init.lua

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,53 @@ local utils = require("nvim-tree.utils")
1111
local Class = require("nvim-tree.classic")
1212
local DirectoryNode = require("nvim-tree.node.directory")
1313

14+
local function get_save_path(opts)
15+
if type(opts.bookmarks.persist) == "string" then
16+
return opts.bookmarks.persist
17+
else
18+
return vim.fn.stdpath("data") .. "/nvim-tree-bookmarks.json"
19+
end
20+
end
21+
22+
local function save_bookmarks(marks, opts)
23+
if not opts.bookmarks.persist then
24+
return
25+
end
26+
27+
local storepath = get_save_path(opts)
28+
local file, errmsg = io.open(storepath, "w")
29+
if file then
30+
local data = {}
31+
for path, _ in pairs(marks) do
32+
table.insert(data, path)
33+
end
34+
file:write(vim.json.encode(data))
35+
file:close()
36+
else
37+
notify.warn(string.format("Invalid bookmarks.persist, disabling persistence: %s", errmsg))
38+
opts.bookmarks.persist = false
39+
end
40+
end
41+
42+
local function load_bookmarks(opts)
43+
local storepath = get_save_path(opts)
44+
local file = io.open(storepath, "r")
45+
if file then
46+
local content = file:read("*all")
47+
file:close()
48+
if content and content ~= "" then
49+
local data = vim.json.decode(content)
50+
local marks = {}
51+
for _, path in ipairs(data) do
52+
-- Store as boolean initially; will be lazily resolved to node on first access
53+
marks[path] = true
54+
end
55+
return marks
56+
end
57+
end
58+
return {}
59+
end
60+
1461
---@class (exact) Marks: Class
1562
---@field private explorer Explorer
1663
---@field private marks table<string, Node> by absolute path
@@ -26,8 +73,15 @@ local Marks = Class:extend()
2673
---@param args MarksArgs
2774
function Marks:new(args)
2875
self.explorer = args.explorer
29-
3076
self.marks = {}
77+
if self.explorer.opts.bookmarks.persist then
78+
local ok, loaded_marks = pcall(load_bookmarks, self.explorer.opts)
79+
if ok then
80+
self.marks = loaded_marks
81+
else
82+
notify.warn(string.format("Failed to load bookmarks: %s", loaded_marks))
83+
end
84+
end
3185
end
3286

3387
---Clear all marks and reload if watchers disabled
@@ -59,6 +113,12 @@ function Marks:toggle(node)
59113
self.marks[node.absolute_path] = node
60114
end
61115

116+
if self.explorer.opts.bookmarks.persist then
117+
local ok, err = pcall(save_bookmarks, self.marks, self.explorer.opts)
118+
if not ok then
119+
notify.warn(string.format("Failed to save bookmarks: %s", err))
120+
end
121+
end
62122
self.explorer.renderer:draw()
63123
end
64124

@@ -67,16 +127,45 @@ end
67127
---@param node Node
68128
---@return Node|nil
69129
function Marks:get(node)
70-
return node and self.marks[node.absolute_path]
130+
if not node or not node.absolute_path then
131+
return nil
132+
end
133+
local mark = self.marks[node.absolute_path]
134+
if mark == true then
135+
-- Lazy resolve: try to find node in explorer tree
136+
local resolved_node = self.explorer:get_node_from_path(node.absolute_path)
137+
if resolved_node then
138+
-- Cache the resolved node
139+
self.marks[node.absolute_path] = resolved_node
140+
return resolved_node
141+
end
142+
return nil
143+
end
144+
return mark
71145
end
72146

73147
---List marked nodes
74148
---@public
75149
---@return Node[]
76150
function Marks:list()
77151
local list = {}
78-
for _, node in pairs(self.marks) do
79-
table.insert(list, node)
152+
for path, mark in pairs(self.marks) do
153+
local node
154+
if mark == true then
155+
-- Lazy resolve: try to find node in explorer tree
156+
node = self.explorer:get_node_from_path(path)
157+
if node then
158+
-- Cache the resolved node for future access
159+
self.marks[path] = node
160+
end
161+
-- If node not found (file deleted/moved), skip it silently
162+
else
163+
-- Already a node object
164+
node = mark
165+
end
166+
if node then
167+
table.insert(list, node)
168+
end
80169
end
81170
return list
82171
end
@@ -90,7 +179,7 @@ function Marks:bulk_delete()
90179
end
91180

92181
local function execute()
93-
for _, node in pairs(self.marks) do
182+
for _, node in ipairs(self:list()) do
94183
remove_file.remove(node)
95184
end
96185
self:clear_reload()
@@ -119,7 +208,7 @@ function Marks:bulk_trash()
119208
end
120209

121210
local function execute()
122-
for _, node in pairs(self.marks) do
211+
for _, node in ipairs(self:list()) do
123212
trash.remove(node)
124213
end
125214
self:clear_reload()
@@ -172,7 +261,7 @@ function Marks:bulk_move()
172261
return
173262
end
174263

175-
for _, node in pairs(self.marks) do
264+
for _, node in ipairs(self:list()) do
176265
local head = vim.fn.fnamemodify(node.absolute_path, ":t")
177266
local to = utils.path_join({ location, head })
178267
rename_file.rename(node, to)
@@ -259,7 +348,17 @@ function Marks:navigate_select()
259348
if not choice or choice == "" then
260349
return
261350
end
262-
local node = self.marks[choice]
351+
local mark = self.marks[choice]
352+
local node
353+
if mark == true then
354+
-- Lazy resolve
355+
node = self.explorer:get_node_from_path(choice)
356+
if node then
357+
self.marks[choice] = node
358+
end
359+
else
360+
node = mark
361+
end
263362
if node and not node:is(DirectoryNode) and not utils.get_win_buf_from_path(node.absolute_path) then
264363
open_file.fn("edit", node.absolute_path)
265364
elseif node then

0 commit comments

Comments
 (0)