diffstalker 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dependency-cruiser.cjs +67 -0
- package/.githooks/pre-commit +2 -0
- package/.githooks/pre-push +15 -0
- package/README.md +43 -35
- package/bun.lock +60 -4
- package/dist/App.js +495 -131
- package/dist/KeyBindings.js +134 -10
- package/dist/MouseHandlers.js +67 -20
- package/dist/core/ExplorerStateManager.js +37 -75
- package/dist/core/GitStateManager.js +252 -46
- package/dist/git/diff.js +99 -18
- package/dist/git/status.js +111 -54
- package/dist/git/test-helpers.js +67 -0
- package/dist/index.js +54 -43
- package/dist/ipc/CommandClient.js +6 -7
- package/dist/state/UIState.js +22 -0
- package/dist/types/remote.js +5 -0
- package/dist/ui/PaneRenderers.js +45 -15
- package/dist/ui/modals/BranchPicker.js +157 -0
- package/dist/ui/modals/CommitActionConfirm.js +66 -0
- package/dist/ui/modals/FileFinder.js +45 -75
- package/dist/ui/modals/HotkeysModal.js +35 -3
- package/dist/ui/modals/SoftResetConfirm.js +68 -0
- package/dist/ui/modals/StashListModal.js +98 -0
- package/dist/ui/modals/ThemePicker.js +1 -2
- package/dist/ui/widgets/CommitPanel.js +113 -7
- package/dist/ui/widgets/CompareListView.js +44 -23
- package/dist/ui/widgets/DiffView.js +216 -170
- package/dist/ui/widgets/ExplorerView.js +50 -54
- package/dist/ui/widgets/FileList.js +62 -95
- package/dist/ui/widgets/FlatFileList.js +65 -0
- package/dist/ui/widgets/Footer.js +25 -15
- package/dist/ui/widgets/Header.js +51 -9
- package/dist/ui/widgets/fileRowFormatters.js +73 -0
- package/dist/utils/ansiTruncate.js +0 -1
- package/dist/utils/displayRows.js +101 -21
- package/dist/utils/flatFileList.js +67 -0
- package/dist/utils/layoutCalculations.js +5 -3
- package/eslint.metrics.js +0 -1
- package/metrics/v0.2.2.json +229 -0
- package/metrics/v0.2.3.json +243 -0
- package/package.json +10 -3
package/dist/KeyBindings.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SPLIT_RATIO_STEP } from './ui/Layout.js';
|
|
2
2
|
import { getFileAtIndex } from './ui/widgets/FileList.js';
|
|
3
|
+
import { getFlatFileAtIndex } from './utils/flatFileList.js';
|
|
3
4
|
/**
|
|
4
5
|
* Register all keyboard bindings on the blessed screen.
|
|
5
6
|
*/
|
|
@@ -41,10 +42,16 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
41
42
|
ctx.uiState.togglePane();
|
|
42
43
|
});
|
|
43
44
|
// Staging operations (skip if modal is open)
|
|
45
|
+
// Context-aware: hunk staging when diff pane is focused on diff tab
|
|
44
46
|
screen.key(['s'], () => {
|
|
45
47
|
if (ctx.hasActiveModal())
|
|
46
48
|
return;
|
|
47
|
-
|
|
49
|
+
if (ctx.getBottomTab() === 'diff' && ctx.getCurrentPane() === 'diff') {
|
|
50
|
+
actions.toggleCurrentHunk();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
actions.stageSelected();
|
|
54
|
+
}
|
|
48
55
|
});
|
|
49
56
|
screen.key(['S-u'], () => {
|
|
50
57
|
if (ctx.hasActiveModal())
|
|
@@ -85,7 +92,7 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
85
92
|
if (ctx.hasActiveModal())
|
|
86
93
|
return;
|
|
87
94
|
if (ctx.getBottomTab() === 'explorer') {
|
|
88
|
-
ctx.
|
|
95
|
+
ctx.getExplorerManager()?.toggleShowOnlyChanges();
|
|
89
96
|
}
|
|
90
97
|
});
|
|
91
98
|
// Explorer: open file finder
|
|
@@ -96,6 +103,12 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
96
103
|
actions.openFileFinder();
|
|
97
104
|
}
|
|
98
105
|
});
|
|
106
|
+
// Ctrl+P: open file finder from any tab
|
|
107
|
+
screen.key(['C-p'], () => {
|
|
108
|
+
if (ctx.hasActiveModal())
|
|
109
|
+
return;
|
|
110
|
+
actions.openFileFinder();
|
|
111
|
+
});
|
|
99
112
|
// Commit (skip if modal is open)
|
|
100
113
|
screen.key(['c'], () => {
|
|
101
114
|
if (ctx.hasActiveModal())
|
|
@@ -117,6 +130,29 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
117
130
|
ctx.uiState.toggleAutoTab();
|
|
118
131
|
}
|
|
119
132
|
});
|
|
133
|
+
// Ctrl+a: toggle amend on commit tab (works even when input is focused)
|
|
134
|
+
screen.key(['C-a'], () => {
|
|
135
|
+
if (ctx.getBottomTab() === 'commit') {
|
|
136
|
+
ctx.commitFlowState.toggleAmend();
|
|
137
|
+
actions.render();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Remote operations (global, any tab)
|
|
141
|
+
screen.key(['S-p'], () => {
|
|
142
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
143
|
+
return;
|
|
144
|
+
actions.push();
|
|
145
|
+
});
|
|
146
|
+
screen.key(['S-f'], () => {
|
|
147
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
148
|
+
return;
|
|
149
|
+
actions.fetchRemote();
|
|
150
|
+
});
|
|
151
|
+
screen.key(['S-r'], () => {
|
|
152
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
153
|
+
return;
|
|
154
|
+
actions.pullRebase();
|
|
155
|
+
});
|
|
120
156
|
screen.key(['escape'], () => {
|
|
121
157
|
if (ctx.getBottomTab() === 'commit') {
|
|
122
158
|
if (ctx.isCommitInputFocused()) {
|
|
@@ -150,29 +186,117 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
150
186
|
screen.key(['?'], () => ctx.uiState.toggleModal('hotkeys'));
|
|
151
187
|
// Follow toggle
|
|
152
188
|
screen.key(['f'], () => actions.toggleFollow());
|
|
153
|
-
// Compare view: base branch picker
|
|
189
|
+
// Compare view: base branch picker / Commit tab: branch picker
|
|
154
190
|
screen.key(['b'], () => {
|
|
191
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
192
|
+
return;
|
|
155
193
|
if (ctx.getBottomTab() === 'compare') {
|
|
156
194
|
ctx.uiState.openModal('baseBranch');
|
|
157
195
|
}
|
|
196
|
+
else if (ctx.getBottomTab() === 'commit') {
|
|
197
|
+
actions.openBranchPicker();
|
|
198
|
+
}
|
|
158
199
|
});
|
|
159
|
-
//
|
|
200
|
+
// u: toggle uncommitted in compare view
|
|
160
201
|
screen.key(['u'], () => {
|
|
202
|
+
if (ctx.hasActiveModal())
|
|
203
|
+
return;
|
|
161
204
|
if (ctx.getBottomTab() === 'compare') {
|
|
162
205
|
ctx.uiState.toggleIncludeUncommitted();
|
|
163
206
|
const includeUncommitted = ctx.uiState.state.includeUncommitted;
|
|
164
|
-
ctx.
|
|
207
|
+
ctx.getGitManager()?.refreshCompareDiff(includeUncommitted);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// Toggle flat file view (diff/commit tab only)
|
|
211
|
+
screen.key(['h'], () => {
|
|
212
|
+
if (ctx.hasActiveModal())
|
|
213
|
+
return;
|
|
214
|
+
const tab = ctx.getBottomTab();
|
|
215
|
+
if (tab === 'diff' || tab === 'commit') {
|
|
216
|
+
ctx.uiState.toggleFlatViewMode();
|
|
165
217
|
}
|
|
166
218
|
});
|
|
167
219
|
// Discard changes (with confirmation)
|
|
168
220
|
screen.key(['d'], () => {
|
|
169
221
|
if (ctx.getBottomTab() === 'diff') {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
222
|
+
if (ctx.uiState.state.flatViewMode) {
|
|
223
|
+
const flatEntry = getFlatFileAtIndex(ctx.getCachedFlatFiles(), ctx.getSelectedIndex());
|
|
224
|
+
if (flatEntry?.unstagedEntry) {
|
|
225
|
+
const file = flatEntry.unstagedEntry;
|
|
226
|
+
if (file.status !== 'untracked') {
|
|
227
|
+
actions.showDiscardConfirm(file);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
175
230
|
}
|
|
231
|
+
else {
|
|
232
|
+
const files = ctx.getStatusFiles();
|
|
233
|
+
const selectedFile = getFileAtIndex(files, ctx.getSelectedIndex());
|
|
234
|
+
// Only allow discard for unstaged modified files
|
|
235
|
+
if (selectedFile && !selectedFile.staged && selectedFile.status !== 'untracked') {
|
|
236
|
+
actions.showDiscardConfirm(selectedFile);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
// Hunk navigation (only when diff pane focused on diff tab)
|
|
242
|
+
screen.key(['n'], () => {
|
|
243
|
+
if (ctx.hasActiveModal())
|
|
244
|
+
return;
|
|
245
|
+
if (ctx.getBottomTab() === 'diff' && ctx.getCurrentPane() === 'diff') {
|
|
246
|
+
actions.navigateNextHunk();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
screen.key(['S-n'], () => {
|
|
250
|
+
if (ctx.hasActiveModal())
|
|
251
|
+
return;
|
|
252
|
+
if (ctx.getBottomTab() === 'diff' && ctx.getCurrentPane() === 'diff') {
|
|
253
|
+
actions.navigatePrevHunk();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
// Stash: save (global)
|
|
257
|
+
screen.key(['S-s'], () => {
|
|
258
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
259
|
+
return;
|
|
260
|
+
actions.stash();
|
|
261
|
+
});
|
|
262
|
+
// Stash: pop (commit tab only)
|
|
263
|
+
screen.key(['o'], () => {
|
|
264
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
265
|
+
return;
|
|
266
|
+
if (ctx.getBottomTab() === 'commit') {
|
|
267
|
+
actions.stashPop();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
// Stash: list modal (commit tab only)
|
|
271
|
+
screen.key(['l'], () => {
|
|
272
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
273
|
+
return;
|
|
274
|
+
if (ctx.getBottomTab() === 'commit') {
|
|
275
|
+
actions.openStashListModal();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
// Soft reset HEAD~1 (commit tab only)
|
|
279
|
+
screen.key(['S-x'], () => {
|
|
280
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
281
|
+
return;
|
|
282
|
+
if (ctx.getBottomTab() === 'commit') {
|
|
283
|
+
actions.showSoftResetConfirm();
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
// Cherry-pick selected commit (history tab only)
|
|
287
|
+
screen.key(['p'], () => {
|
|
288
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
289
|
+
return;
|
|
290
|
+
if (ctx.getBottomTab() === 'history') {
|
|
291
|
+
actions.cherryPickSelected();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
// Revert selected commit (history tab only)
|
|
295
|
+
screen.key(['v'], () => {
|
|
296
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
297
|
+
return;
|
|
298
|
+
if (ctx.getBottomTab() === 'history') {
|
|
299
|
+
actions.revertSelected();
|
|
176
300
|
}
|
|
177
301
|
});
|
|
178
302
|
}
|
package/dist/MouseHandlers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getFileListTotalRows, getFileIndexFromRow } from './ui/widgets/FileList.js';
|
|
2
|
+
import { getFlatFileListTotalRows } from './ui/widgets/FlatFileList.js';
|
|
2
3
|
import { getCompareListTotalRows, getCompareSelectionFromRow, } from './ui/widgets/CompareListView.js';
|
|
3
4
|
import { getExplorerTotalRows } from './ui/widgets/ExplorerView.js';
|
|
4
5
|
import { getExplorerContentTotalRows } from './ui/widgets/ExplorerContent.js';
|
|
@@ -28,11 +29,60 @@ export function setupMouseHandlers(layout, actions, ctx) {
|
|
|
28
29
|
handleTopPaneClick(clickedRow, mouse.x, actions, ctx);
|
|
29
30
|
}
|
|
30
31
|
});
|
|
32
|
+
// Click on bottom pane
|
|
33
|
+
layout.bottomPane.on('click', (mouse) => {
|
|
34
|
+
const clickedRow = layout.screenYToBottomPaneRow(mouse.y);
|
|
35
|
+
if (clickedRow >= 0) {
|
|
36
|
+
if (ctx.uiState.state.bottomTab === 'commit') {
|
|
37
|
+
// Row 6 (0-indexed) is the amend checkbox row
|
|
38
|
+
if (clickedRow === 6) {
|
|
39
|
+
actions.toggleAmend();
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
actions.focusCommitInput();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
actions.selectHunkAtRow(clickedRow);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
31
50
|
// Click on footer for tabs and toggles
|
|
32
51
|
layout.footerBox.on('click', (mouse) => {
|
|
33
52
|
handleFooterClick(mouse.x, actions, ctx);
|
|
34
53
|
});
|
|
35
54
|
}
|
|
55
|
+
function handleFileListClick(row, x, actions, ctx) {
|
|
56
|
+
const state = ctx.uiState.state;
|
|
57
|
+
if (state.flatViewMode) {
|
|
58
|
+
// Flat mode: row 0 is header, files start at row 1
|
|
59
|
+
const absoluteRow = row + state.fileListScrollOffset;
|
|
60
|
+
const fileIndex = absoluteRow - 1; // subtract header row
|
|
61
|
+
const flatFiles = ctx.getCachedFlatFiles();
|
|
62
|
+
if (fileIndex < 0 || fileIndex >= flatFiles.length)
|
|
63
|
+
return;
|
|
64
|
+
if (x !== undefined && x >= 2 && x <= 4) {
|
|
65
|
+
actions.toggleFileByIndex(fileIndex);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
ctx.uiState.setSelectedIndex(fileIndex);
|
|
69
|
+
actions.selectFileByIndex(fileIndex);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const files = ctx.getStatusFiles();
|
|
74
|
+
const fileIndex = getFileIndexFromRow(row + state.fileListScrollOffset, files);
|
|
75
|
+
if (fileIndex === null || fileIndex < 0)
|
|
76
|
+
return;
|
|
77
|
+
if (x !== undefined && x >= 2 && x <= 4) {
|
|
78
|
+
actions.toggleFileByIndex(fileIndex);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
ctx.uiState.setSelectedIndex(fileIndex);
|
|
82
|
+
actions.selectFileByIndex(fileIndex);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
36
86
|
function handleTopPaneClick(row, x, actions, ctx) {
|
|
37
87
|
const state = ctx.uiState.state;
|
|
38
88
|
if (state.bottomTab === 'history') {
|
|
@@ -50,23 +100,19 @@ function handleTopPaneClick(row, x, actions, ctx) {
|
|
|
50
100
|
}
|
|
51
101
|
else if (state.bottomTab === 'explorer') {
|
|
52
102
|
const index = state.explorerScrollOffset + row;
|
|
53
|
-
ctx.
|
|
54
|
-
|
|
103
|
+
const explorerManager = ctx.getExplorerManager();
|
|
104
|
+
const isAlreadySelected = explorerManager?.state.selectedIndex === index;
|
|
105
|
+
const displayRow = explorerManager?.state.displayRows[index];
|
|
106
|
+
if (isAlreadySelected && displayRow?.node.isDirectory) {
|
|
107
|
+
actions.enterExplorerDirectory();
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
explorerManager?.selectIndex(index);
|
|
111
|
+
ctx.uiState.setExplorerSelectedIndex(index);
|
|
112
|
+
}
|
|
55
113
|
}
|
|
56
114
|
else {
|
|
57
|
-
|
|
58
|
-
const files = ctx.getStatusFiles();
|
|
59
|
-
const fileIndex = getFileIndexFromRow(row + state.fileListScrollOffset, files);
|
|
60
|
-
if (fileIndex !== null && fileIndex >= 0) {
|
|
61
|
-
// Check if click is on the action button [+] or [-] (columns 2-4)
|
|
62
|
-
if (x !== undefined && x >= 2 && x <= 4) {
|
|
63
|
-
actions.toggleFileByIndex(fileIndex);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
ctx.uiState.setSelectedIndex(fileIndex);
|
|
67
|
-
actions.selectFileByIndex(fileIndex);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
115
|
+
handleFileListClick(row, x, actions, ctx);
|
|
70
116
|
}
|
|
71
117
|
}
|
|
72
118
|
function handleFooterClick(x, actions, ctx) {
|
|
@@ -102,7 +148,7 @@ function handleFooterClick(x, actions, ctx) {
|
|
|
102
148
|
actions.toggleFollow();
|
|
103
149
|
}
|
|
104
150
|
else if (x >= 34 && x <= 43 && ctx.uiState.state.bottomTab === 'explorer') {
|
|
105
|
-
ctx.
|
|
151
|
+
ctx.getExplorerManager()?.toggleShowOnlyChanges();
|
|
106
152
|
}
|
|
107
153
|
else if (x === 0) {
|
|
108
154
|
ctx.uiState.openModal('hotkeys');
|
|
@@ -124,14 +170,15 @@ function handleTopPaneScroll(delta, layout, ctx) {
|
|
|
124
170
|
ctx.uiState.setCompareScrollOffset(newOffset);
|
|
125
171
|
}
|
|
126
172
|
else if (state.bottomTab === 'explorer') {
|
|
127
|
-
const totalRows = getExplorerTotalRows(ctx.
|
|
173
|
+
const totalRows = getExplorerTotalRows(ctx.getExplorerManager()?.state.displayRows ?? []);
|
|
128
174
|
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
129
175
|
const newOffset = Math.min(maxOffset, Math.max(0, state.explorerScrollOffset + delta));
|
|
130
176
|
ctx.uiState.setExplorerScrollOffset(newOffset);
|
|
131
177
|
}
|
|
132
178
|
else {
|
|
133
|
-
const
|
|
134
|
-
|
|
179
|
+
const totalRows = state.flatViewMode
|
|
180
|
+
? getFlatFileListTotalRows(ctx.getCachedFlatFiles())
|
|
181
|
+
: getFileListTotalRows(ctx.getStatusFiles());
|
|
135
182
|
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
136
183
|
const newOffset = Math.min(maxOffset, Math.max(0, state.fileListScrollOffset + delta));
|
|
137
184
|
ctx.uiState.setFileListScrollOffset(newOffset);
|
|
@@ -142,7 +189,7 @@ function handleBottomPaneScroll(delta, layout, ctx) {
|
|
|
142
189
|
const visibleHeight = layout.dimensions.bottomPaneHeight;
|
|
143
190
|
const width = ctx.getScreenWidth();
|
|
144
191
|
if (state.bottomTab === 'explorer') {
|
|
145
|
-
const selectedFile = ctx.
|
|
192
|
+
const selectedFile = ctx.getExplorerManager()?.state.selectedFile;
|
|
146
193
|
const totalRows = getExplorerContentTotalRows(selectedFile?.content ?? null, selectedFile?.path ?? null, selectedFile?.truncated ?? false, width, state.wrapMode);
|
|
147
194
|
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
148
195
|
const newOffset = Math.min(maxOffset, Math.max(0, state.explorerFileScrollOffset + delta));
|
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
4
|
import { getIgnoredFiles } from '../git/ignoreUtils.js';
|
|
5
|
+
import { listAllFiles } from '../git/status.js';
|
|
5
6
|
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
6
7
|
const WARN_FILE_SIZE = 100 * 1024; // 100KB
|
|
7
8
|
/**
|
|
@@ -25,6 +26,7 @@ export class ExplorerStateManager extends EventEmitter {
|
|
|
25
26
|
options;
|
|
26
27
|
expandedPaths = new Set();
|
|
27
28
|
gitStatusMap = { files: new Map(), directories: new Set() };
|
|
29
|
+
_cachedFilePaths = null;
|
|
28
30
|
_state = {
|
|
29
31
|
currentPath: '',
|
|
30
32
|
tree: null,
|
|
@@ -61,9 +63,12 @@ export class ExplorerStateManager extends EventEmitter {
|
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
65
|
* Update git status map and refresh display.
|
|
66
|
+
* Also invalidates the file path cache so the next file finder open gets fresh data.
|
|
64
67
|
*/
|
|
65
68
|
setGitStatus(statusMap) {
|
|
66
69
|
this.gitStatusMap = statusMap;
|
|
70
|
+
// Invalidate file path cache — reload in background
|
|
71
|
+
this.loadFilePaths();
|
|
67
72
|
// Refresh display to show updated status
|
|
68
73
|
if (this._state.tree) {
|
|
69
74
|
this.applyGitStatusToTree(this._state.tree);
|
|
@@ -123,7 +128,7 @@ export class ExplorerStateManager extends EventEmitter {
|
|
|
123
128
|
/**
|
|
124
129
|
* Build a tree node for a directory path.
|
|
125
130
|
*/
|
|
126
|
-
async buildTreeNode(relativePath,
|
|
131
|
+
async buildTreeNode(relativePath, _depth) {
|
|
127
132
|
try {
|
|
128
133
|
const fullPath = path.join(this.repoPath, relativePath);
|
|
129
134
|
const stats = await fs.promises.stat(fullPath);
|
|
@@ -153,7 +158,7 @@ export class ExplorerStateManager extends EventEmitter {
|
|
|
153
158
|
}
|
|
154
159
|
return node;
|
|
155
160
|
}
|
|
156
|
-
catch
|
|
161
|
+
catch {
|
|
157
162
|
return null;
|
|
158
163
|
}
|
|
159
164
|
}
|
|
@@ -211,7 +216,7 @@ export class ExplorerStateManager extends EventEmitter {
|
|
|
211
216
|
this.collapseNode(node, children);
|
|
212
217
|
node.childrenLoaded = true;
|
|
213
218
|
}
|
|
214
|
-
catch
|
|
219
|
+
catch {
|
|
215
220
|
node.childrenLoaded = true;
|
|
216
221
|
node.children = [];
|
|
217
222
|
}
|
|
@@ -262,43 +267,21 @@ export class ExplorerStateManager extends EventEmitter {
|
|
|
262
267
|
/**
|
|
263
268
|
* Flatten tree into display rows.
|
|
264
269
|
*/
|
|
270
|
+
shouldIncludeNode(node) {
|
|
271
|
+
if (!this.options.showOnlyChanges)
|
|
272
|
+
return true;
|
|
273
|
+
if (node.isDirectory)
|
|
274
|
+
return !!node.hasChangedChildren;
|
|
275
|
+
return !!node.gitStatus;
|
|
276
|
+
}
|
|
265
277
|
flattenTree(root) {
|
|
266
278
|
const rows = [];
|
|
267
|
-
const
|
|
268
|
-
// Skip root node in display (but process its children)
|
|
269
|
-
if (depth === 0) {
|
|
270
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
271
|
-
const child = node.children[i];
|
|
272
|
-
const isLast = i === node.children.length - 1;
|
|
273
|
-
// Apply filter if showOnlyChanges is enabled
|
|
274
|
-
if (this.options.showOnlyChanges) {
|
|
275
|
-
if (child.isDirectory && !child.hasChangedChildren)
|
|
276
|
-
continue;
|
|
277
|
-
if (!child.isDirectory && !child.gitStatus)
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
rows.push({
|
|
281
|
-
node: child,
|
|
282
|
-
depth: 0,
|
|
283
|
-
isLast,
|
|
284
|
-
parentIsLast: [],
|
|
285
|
-
});
|
|
286
|
-
if (child.isDirectory && child.expanded) {
|
|
287
|
-
traverse(child, 1, [isLast]);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
279
|
+
const traverseChildren = (node, depth, parentIsLast) => {
|
|
292
280
|
for (let i = 0; i < node.children.length; i++) {
|
|
293
281
|
const child = node.children[i];
|
|
294
282
|
const isLast = i === node.children.length - 1;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (child.isDirectory && !child.hasChangedChildren)
|
|
298
|
-
continue;
|
|
299
|
-
if (!child.isDirectory && !child.gitStatus)
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
283
|
+
if (!this.shouldIncludeNode(child))
|
|
284
|
+
continue;
|
|
302
285
|
rows.push({
|
|
303
286
|
node: child,
|
|
304
287
|
depth,
|
|
@@ -306,11 +289,12 @@ export class ExplorerStateManager extends EventEmitter {
|
|
|
306
289
|
parentIsLast: [...parentIsLast],
|
|
307
290
|
});
|
|
308
291
|
if (child.isDirectory && child.expanded) {
|
|
309
|
-
|
|
292
|
+
traverseChildren(child, depth + 1, [...parentIsLast, isLast]);
|
|
310
293
|
}
|
|
311
294
|
}
|
|
312
295
|
};
|
|
313
|
-
|
|
296
|
+
// Start from root's children at depth 0 (root itself is not displayed)
|
|
297
|
+
traverseChildren(root, 0, []);
|
|
314
298
|
return rows;
|
|
315
299
|
}
|
|
316
300
|
/**
|
|
@@ -559,45 +543,23 @@ export class ExplorerStateManager extends EventEmitter {
|
|
|
559
543
|
return search(this._state.tree);
|
|
560
544
|
}
|
|
561
545
|
/**
|
|
562
|
-
*
|
|
563
|
-
*
|
|
546
|
+
* Load all file paths using git ls-files (fast, single git command).
|
|
547
|
+
* Stores result in cache for instant access by FileFinder.
|
|
564
548
|
*/
|
|
565
|
-
async
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (this.options.hideHidden && entry.name.startsWith('.')) {
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
const entryPath = dirPath ? path.join(dirPath, entry.name) : entry.name;
|
|
583
|
-
// Filter gitignored files
|
|
584
|
-
if (this.options.hideGitignored && ignoredFiles.has(entryPath)) {
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
if (entry.isDirectory()) {
|
|
588
|
-
await scanDir(entryPath);
|
|
589
|
-
}
|
|
590
|
-
else {
|
|
591
|
-
paths.push(entryPath);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
catch (err) {
|
|
596
|
-
// Ignore errors for individual directories
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
|
-
await scanDir('');
|
|
600
|
-
return paths;
|
|
549
|
+
async loadFilePaths() {
|
|
550
|
+
try {
|
|
551
|
+
this._cachedFilePaths = await listAllFiles(this.repoPath);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
this._cachedFilePaths = [];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Get cached file paths (for file finder).
|
|
559
|
+
* Returns empty array if not yet loaded.
|
|
560
|
+
*/
|
|
561
|
+
getCachedFilePaths() {
|
|
562
|
+
return this._cachedFilePaths ?? [];
|
|
601
563
|
}
|
|
602
564
|
/**
|
|
603
565
|
* Navigate to a specific file path in the tree.
|