@@ -8,8 +8,15 @@ local explorer_node = require "nvim-tree.explorer.node"
8
8
9
9
local M = {
10
10
config = {},
11
- projects = {},
12
- cwd_to_project_root = {},
11
+
12
+ -- all projects keyed by toplevel
13
+ _projects_by_toplevel = {},
14
+
15
+ -- index of paths inside toplevels, false when not inside a project
16
+ _toplevels_by_path = {},
17
+
18
+ -- git dirs by toplevel
19
+ _git_dirs_by_toplevel = {},
13
20
}
14
21
15
22
-- Files under .git that should result in a reload when changed.
@@ -22,7 +29,7 @@ local WATCHED_FILES = {
22
29
" index" , -- staging area
23
30
}
24
31
25
- local function reload_git_status (project_root , path , project , git_status )
32
+ local function reload_git_status (toplevel , path , project , git_status )
26
33
if path then
27
34
for p in pairs (project .files ) do
28
35
if p :find (path , 1 , true ) == 1 then
@@ -34,7 +41,7 @@ local function reload_git_status(project_root, path, project, git_status)
34
41
project .files = git_status
35
42
end
36
43
37
- project .dirs = git_utils .file_status_to_dir_status (project .files , project_root )
44
+ project .dirs = git_utils .file_status_to_dir_status (project .files , toplevel )
38
45
end
39
46
40
47
--- Is this path in a known ignored directory?
@@ -56,85 +63,106 @@ local function path_ignored_in_project(path, project)
56
63
return false
57
64
end
58
65
66
+ --- Reload all projects
67
+ --- @return table projects maybe empty
59
68
function M .reload ()
60
69
if not M .config .git .enable then
61
70
return {}
62
71
end
63
72
64
- for project_root in pairs (M .projects ) do
65
- M .reload_project (project_root )
73
+ for toplevel in pairs (M ._projects_by_toplevel ) do
74
+ M .reload_project (toplevel )
66
75
end
67
76
68
- return M .projects
77
+ return M ._projects_by_toplevel
69
78
end
70
79
71
- function M .reload_project (project_root , path , callback )
72
- local project = M .projects [project_root ]
73
- if not project or not M .config .git .enable then
80
+ --- Reload one project. Does nothing when no project or path is ignored
81
+ --- @param toplevel string | nil
82
+ --- @param path string | nil optional path to update only
83
+ --- @param callback function | nil
84
+ function M .reload_project (toplevel , path , callback )
85
+ local project = M ._projects_by_toplevel [toplevel ]
86
+ if not toplevel or not project or not M .config .git .enable then
74
87
if callback then
75
88
callback ()
76
89
end
77
90
return
78
91
end
79
92
80
- if path and (path :find (project_root , 1 , true ) ~= 1 or path_ignored_in_project (path , project )) then
93
+ if path and (path :find (toplevel , 1 , true ) ~= 1 or path_ignored_in_project (path , project )) then
81
94
if callback then
82
95
callback ()
83
96
end
84
97
return
85
98
end
86
99
87
100
local opts = {
88
- project_root = project_root ,
101
+ toplevel = toplevel ,
89
102
path = path ,
90
- list_untracked = git_utils .should_show_untracked (project_root ),
103
+ list_untracked = git_utils .should_show_untracked (toplevel ),
91
104
list_ignored = true ,
92
105
timeout = M .config .git .timeout ,
93
106
}
94
107
95
108
if callback then
96
109
Runner .run (opts , function (git_status )
97
- reload_git_status (project_root , path , project , git_status )
110
+ reload_git_status (toplevel , path , project , git_status )
98
111
callback ()
99
112
end )
100
113
else
101
114
-- TODO use callback once async/await is available
102
115
local git_status = Runner .run (opts )
103
- reload_git_status (project_root , path , project , git_status )
116
+ reload_git_status (toplevel , path , project , git_status )
104
117
end
105
118
end
106
119
107
- function M .get_project (project_root )
108
- return M .projects [project_root ]
120
+ --- Retrieve a known project
121
+ --- @return table | nil project
122
+ function M .get_project (toplevel )
123
+ return M ._projects_by_toplevel [toplevel ]
109
124
end
110
125
111
- function M .get_project_root (cwd )
126
+ --- Retrieve the toplevel for a path. nil on:
127
+ --- git disabled
128
+ --- not part of a project
129
+ --- not a directory
130
+ --- path in git.disable_for_dirs
131
+ --- @param path string absolute
132
+ --- @return string | nil
133
+ function M .get_toplevel (path )
112
134
if not M .config .git .enable then
113
135
return nil
114
136
end
115
137
116
- if M .cwd_to_project_root [ cwd ] then
117
- return M .cwd_to_project_root [ cwd ]
138
+ if M ._toplevels_by_path [ path ] then
139
+ return M ._toplevels_by_path [ path ]
118
140
end
119
141
120
- if M .cwd_to_project_root [ cwd ] == false then
142
+ if M ._toplevels_by_path [ path ] == false then
121
143
return nil
122
144
end
123
145
124
- local stat , _ = vim .loop .fs_stat (cwd )
146
+ local stat , _ = vim .loop .fs_stat (path )
125
147
if not stat or stat .type ~= " directory" then
126
148
return nil
127
149
end
128
150
129
151
-- short-circuit any known ignored paths
130
- for root , project in pairs (M .projects ) do
131
- if project and path_ignored_in_project (cwd , project ) then
132
- M .cwd_to_project_root [ cwd ] = root
152
+ for root , project in pairs (M ._projects_by_toplevel ) do
153
+ if project and path_ignored_in_project (path , project ) then
154
+ M ._toplevels_by_path [ path ] = root
133
155
return root
134
156
end
135
157
end
136
158
137
- local toplevel = git_utils .get_toplevel (cwd )
159
+ -- attempt to fetch toplevel
160
+ local toplevel , git_dir = git_utils .get_toplevel (path )
161
+ if not toplevel or not git_dir then
162
+ return nil
163
+ end
164
+
165
+ -- ignore disabled paths
138
166
for _ , disabled_for_dir in ipairs (M .config .git .disable_for_dirs ) do
139
167
local toplevel_norm = vim .fn .fnamemodify (toplevel , " :p" )
140
168
local disabled_norm = vim .fn .fnamemodify (disabled_for_dir , " :p" )
@@ -143,23 +171,24 @@ function M.get_project_root(cwd)
143
171
end
144
172
end
145
173
146
- M .cwd_to_project_root [cwd ] = toplevel
147
- return M .cwd_to_project_root [cwd ]
174
+ M ._toplevels_by_path [path ] = toplevel
175
+ M ._git_dirs_by_toplevel [toplevel ] = git_dir
176
+ return M ._toplevels_by_path [path ]
148
177
end
149
178
150
- local function reload_tree_at (project_root )
151
- if not M .config .git .enable then
179
+ local function reload_tree_at (toplevel )
180
+ if not M .config .git .enable or not toplevel then
152
181
return nil
153
182
end
154
183
155
- log .line (" watcher" , " git event executing '%s'" , project_root )
156
- local root_node = utils .get_node_from_path (project_root )
184
+ log .line (" watcher" , " git event executing '%s'" , toplevel )
185
+ local root_node = utils .get_node_from_path (toplevel )
157
186
if not root_node then
158
187
return
159
188
end
160
189
161
- M .reload_project (project_root , nil , function ()
162
- local git_status = M .get_project (project_root )
190
+ M .reload_project (toplevel , nil , function ()
191
+ local git_status = M .get_project (toplevel )
163
192
164
193
Iterator .builder (root_node .nodes )
165
194
:hidden ()
@@ -176,25 +205,29 @@ local function reload_tree_at(project_root)
176
205
end )
177
206
end
178
207
179
- function M .load_project_status (cwd )
208
+ --- Load the project status for a path. Does nothing when no toplevel for path.
209
+ --- Only fetches project status when unknown, otherwise returns existing.
210
+ --- @param path string absolute
211
+ --- @return table project maybe empty
212
+ function M .load_project_status (path )
180
213
if not M .config .git .enable then
181
214
return {}
182
215
end
183
216
184
- local project_root = M .get_project_root ( cwd )
185
- if not project_root then
186
- M .cwd_to_project_root [ cwd ] = false
217
+ local toplevel = M .get_toplevel ( path )
218
+ if not toplevel then
219
+ M ._toplevels_by_path [ path ] = false
187
220
return {}
188
221
end
189
222
190
- local status = M .projects [ project_root ]
223
+ local status = M ._projects_by_toplevel [ toplevel ]
191
224
if status then
192
225
return status
193
226
end
194
227
195
228
local git_status = Runner .run {
196
- project_root = project_root ,
197
- list_untracked = git_utils .should_show_untracked (project_root ),
229
+ toplevel = toplevel ,
230
+ list_untracked = git_utils .should_show_untracked (toplevel ),
198
231
list_ignored = true ,
199
232
timeout = M .config .git .timeout ,
200
233
}
@@ -204,33 +237,41 @@ function M.load_project_status(cwd)
204
237
log .line (" watcher" , " git start" )
205
238
206
239
local callback = function (w )
207
- log .line (" watcher" , " git event scheduled '%s'" , w .project_root )
208
- utils .debounce (" git:watcher:" .. w .project_root , M .config .filesystem_watchers .debounce_delay , function ()
240
+ log .line (" watcher" , " git event scheduled '%s'" , w .toplevel )
241
+ utils .debounce (" git:watcher:" .. w .toplevel , M .config .filesystem_watchers .debounce_delay , function ()
209
242
if w .destroyed then
210
243
return
211
244
end
212
- reload_tree_at (w .project_root )
245
+ reload_tree_at (w .toplevel )
213
246
end )
214
247
end
215
248
216
- local git_dir = vim .env .GIT_DIR or utils .path_join { project_root , " .git" }
249
+ local git_dir = vim .env .GIT_DIR or M . _git_dirs_by_toplevel [ toplevel ] or utils .path_join { toplevel , " .git" }
217
250
watcher = Watcher :new (git_dir , WATCHED_FILES , callback , {
218
- project_root = project_root ,
251
+ toplevel = toplevel ,
219
252
})
220
253
end
221
254
222
- M .projects [ project_root ] = {
255
+ M ._projects_by_toplevel [ toplevel ] = {
223
256
files = git_status ,
224
- dirs = git_utils .file_status_to_dir_status (git_status , project_root ),
257
+ dirs = git_utils .file_status_to_dir_status (git_status , toplevel ),
225
258
watcher = watcher ,
226
259
}
227
- return M .projects [ project_root ]
260
+ return M ._projects_by_toplevel [ toplevel ]
228
261
end
229
262
230
263
function M .purge_state ()
231
264
log .line (" git" , " purge_state" )
232
- M .projects = {}
233
- M .cwd_to_project_root = {}
265
+
266
+ for _ , project in pairs (M ._projects_by_toplevel ) do
267
+ if project .watcher then
268
+ project .watcher :destroy ()
269
+ end
270
+ end
271
+
272
+ M ._projects_by_toplevel = {}
273
+ M ._toplevels_by_path = {}
274
+ M ._git_dirs_by_toplevel = {}
234
275
end
235
276
236
277
--- Disable git integration permanently
0 commit comments