@tmustier/pi-files-widget 0.1.0 → 0.1.7
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/CHANGELOG.md +7 -0
- package/browser.ts +43 -34
- package/constants.ts +1 -0
- package/file-tree.ts +40 -13
- package/package.json +1 -1
- package/types.ts +1 -0
- package/viewer.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.7] - 2026-01-26
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Cache line counts and skip large files to avoid freezes in big folders
|
|
9
|
+
- Avoid recomputing tree stats on every render
|
|
10
|
+
- Preserve line counts for open files across refreshes
|
|
11
|
+
|
|
5
12
|
## [0.1.6] - 2026-01-24
|
|
6
13
|
|
|
7
14
|
### Added
|
package/browser.ts
CHANGED
|
@@ -21,10 +21,17 @@ export interface BrowserController {
|
|
|
21
21
|
invalidate(): void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
interface BrowserStats {
|
|
25
|
+
totalLines?: number;
|
|
26
|
+
additions: number;
|
|
27
|
+
deletions: number;
|
|
28
|
+
}
|
|
24
29
|
|
|
25
30
|
interface BrowserState {
|
|
26
31
|
root: FileNode | null;
|
|
27
32
|
flatList: FlatNode[];
|
|
33
|
+
fullList: FlatNode[];
|
|
34
|
+
stats: BrowserStats;
|
|
28
35
|
selectedIndex: number;
|
|
29
36
|
searchQuery: string;
|
|
30
37
|
searchMode: boolean;
|
|
@@ -100,28 +107,16 @@ function collectChangedFiles(node: FileNode, ancestors: FileNode[] = []): Change
|
|
|
100
107
|
return results;
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let deletions = 0;
|
|
107
|
-
|
|
108
|
-
function traverse(node: FileNode): void {
|
|
109
|
-
if (!node.isDirectory) {
|
|
110
|
-
totalLines += node.lineCount || 0;
|
|
111
|
-
if (node.diffStats) {
|
|
112
|
-
additions += node.diffStats.additions;
|
|
113
|
-
deletions += node.diffStats.deletions;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (node.children) {
|
|
117
|
-
for (const child of node.children) {
|
|
118
|
-
traverse(child);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
110
|
+
function getTreeStats(root: FileNode | null): BrowserStats {
|
|
111
|
+
if (!root) {
|
|
112
|
+
return { totalLines: undefined, additions: 0, deletions: 0 };
|
|
121
113
|
}
|
|
122
114
|
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
return {
|
|
116
|
+
totalLines: root.lineCountComplete ? root.totalLines ?? 0 : undefined,
|
|
117
|
+
additions: root.totalAdditions ?? 0,
|
|
118
|
+
deletions: root.totalDeletions ?? 0,
|
|
119
|
+
};
|
|
125
120
|
}
|
|
126
121
|
|
|
127
122
|
function formatNodeStatus(node: FileNode, theme: Theme): string {
|
|
@@ -146,7 +141,7 @@ function formatNodeMeta(node: FileNode, theme: Theme): string {
|
|
|
146
141
|
if (node.totalDeletions && node.totalDeletions > 0) {
|
|
147
142
|
parts.push(theme.fg("error", `-${node.totalDeletions}`));
|
|
148
143
|
}
|
|
149
|
-
if (node.totalLines) {
|
|
144
|
+
if (node.totalLines && node.lineCountComplete !== false) {
|
|
150
145
|
parts.push(theme.fg("dim", `${node.totalLines}L`));
|
|
151
146
|
}
|
|
152
147
|
} else if (!node.isDirectory) {
|
|
@@ -157,10 +152,10 @@ function formatNodeMeta(node: FileNode, theme: Theme): string {
|
|
|
157
152
|
if (node.diffStats.deletions > 0) {
|
|
158
153
|
parts.push(theme.fg("error", `-${node.diffStats.deletions}`));
|
|
159
154
|
}
|
|
160
|
-
} else if (isUntrackedStatus(node.gitStatus) && node.lineCount) {
|
|
155
|
+
} else if (isUntrackedStatus(node.gitStatus) && node.lineCount !== undefined) {
|
|
161
156
|
parts.push(theme.fg("success", `+${node.lineCount}`));
|
|
162
157
|
}
|
|
163
|
-
if (node.lineCount) {
|
|
158
|
+
if (node.lineCount !== undefined) {
|
|
164
159
|
parts.push(theme.fg("dim", `${node.lineCount}L`));
|
|
165
160
|
}
|
|
166
161
|
}
|
|
@@ -202,9 +197,13 @@ export function createFileBrowser(
|
|
|
202
197
|
|
|
203
198
|
const viewer = createViewer(cwd, theme, requestComment);
|
|
204
199
|
|
|
200
|
+
const root = buildFileTree(cwd, cwd, gitStatus, diffStats, ignored, agentModifiedFiles);
|
|
201
|
+
|
|
205
202
|
const browser: BrowserState = {
|
|
206
|
-
root
|
|
203
|
+
root,
|
|
207
204
|
flatList: [],
|
|
205
|
+
fullList: [],
|
|
206
|
+
stats: getTreeStats(root),
|
|
208
207
|
selectedIndex: 0,
|
|
209
208
|
searchQuery: "",
|
|
210
209
|
searchMode: false,
|
|
@@ -214,40 +213,49 @@ export function createFileBrowser(
|
|
|
214
213
|
};
|
|
215
214
|
|
|
216
215
|
browser.flatList = browser.root ? flattenTree(browser.root) : [];
|
|
216
|
+
browser.fullList = browser.root ? flattenTree(browser.root, 0, true, true) : [];
|
|
217
217
|
|
|
218
218
|
function refreshGitStatus(): void {
|
|
219
|
-
const
|
|
220
|
-
const
|
|
219
|
+
const previousDisplayList = getDisplayList();
|
|
220
|
+
const currentPath = previousDisplayList[browser.selectedIndex]?.node.path;
|
|
221
|
+
const viewingFile = viewer.getFile();
|
|
222
|
+
const viewingFilePath = viewingFile?.path;
|
|
221
223
|
|
|
222
224
|
const expandedPaths = captureExpandedPaths(browser.root);
|
|
223
225
|
|
|
224
226
|
gitStatus = getGitStatus(cwd);
|
|
225
227
|
diffStats = getGitDiffStats(cwd);
|
|
226
228
|
browser.root = buildFileTree(cwd, cwd, gitStatus, diffStats, ignored, agentModifiedFiles);
|
|
229
|
+
browser.stats = getTreeStats(browser.root);
|
|
227
230
|
|
|
228
231
|
restoreExpandedPaths(browser.root, expandedPaths);
|
|
229
232
|
|
|
230
233
|
browser.flatList = browser.root ? flattenTree(browser.root) : [];
|
|
234
|
+
browser.fullList = browser.root ? flattenTree(browser.root, 0, true, true) : [];
|
|
231
235
|
|
|
236
|
+
const updatedDisplayList = getDisplayList();
|
|
232
237
|
if (currentPath) {
|
|
233
|
-
const newIdx =
|
|
238
|
+
const newIdx = updatedDisplayList.findIndex(f => f.node.path === currentPath);
|
|
234
239
|
if (newIdx !== -1) {
|
|
235
240
|
browser.selectedIndex = newIdx;
|
|
236
241
|
}
|
|
237
242
|
}
|
|
238
243
|
|
|
239
|
-
browser.selectedIndex = Math.min(browser.selectedIndex, Math.max(0,
|
|
244
|
+
browser.selectedIndex = Math.min(browser.selectedIndex, Math.max(0, updatedDisplayList.length - 1));
|
|
240
245
|
|
|
241
246
|
if (viewingFilePath && browser.root) {
|
|
242
247
|
const newNode = findNodeByPath(browser.root, viewingFilePath);
|
|
243
248
|
if (newNode) {
|
|
249
|
+
if (newNode.lineCount === undefined && viewingFile?.lineCount !== undefined) {
|
|
250
|
+
newNode.lineCount = viewingFile.lineCount;
|
|
251
|
+
}
|
|
244
252
|
viewer.updateFileRef(newNode);
|
|
245
253
|
}
|
|
246
254
|
}
|
|
247
255
|
}
|
|
248
256
|
|
|
249
257
|
function getDisplayList(): FlatNode[] {
|
|
250
|
-
let list = browser.flatList;
|
|
258
|
+
let list = browser.searchQuery ? browser.fullList : browser.flatList;
|
|
251
259
|
|
|
252
260
|
if (browser.showOnlyChanged) {
|
|
253
261
|
list = list.filter(f =>
|
|
@@ -321,13 +329,14 @@ export function createFileBrowser(
|
|
|
321
329
|
const lines: string[] = [];
|
|
322
330
|
const pathDisplay = basename(cwd);
|
|
323
331
|
const branchDisplay = gitBranch ? theme.fg("accent", ` (${gitBranch})`) : "";
|
|
324
|
-
const stats =
|
|
332
|
+
const stats = browser.stats;
|
|
325
333
|
|
|
326
|
-
let statsDisplay =
|
|
327
|
-
if (stats.
|
|
328
|
-
|
|
329
|
-
if (stats.deletions > 0) statsDisplay += theme.fg("error", ` -${stats.deletions}`);
|
|
334
|
+
let statsDisplay = "";
|
|
335
|
+
if (stats.totalLines !== undefined) {
|
|
336
|
+
statsDisplay += theme.fg("dim", ` ${stats.totalLines}L`);
|
|
330
337
|
}
|
|
338
|
+
if (stats.additions > 0) statsDisplay += theme.fg("success", ` +${stats.additions}`);
|
|
339
|
+
if (stats.deletions > 0) statsDisplay += theme.fg("error", ` -${stats.deletions}`);
|
|
331
340
|
|
|
332
341
|
const searchIndicator = browser.searchMode
|
|
333
342
|
? theme.fg("accent", ` /${browser.searchQuery}█`)
|
package/constants.ts
CHANGED
package/file-tree.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, join, relative } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { MAX_TREE_DEPTH } from "./constants";
|
|
4
|
+
import { MAX_LINE_COUNT_BYTES, MAX_TREE_DEPTH } from "./constants";
|
|
5
5
|
import type { DiffStats, FileNode, FlatNode } from "./types";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const lineCountCache = new Map<string, { mtimeMs: number; size: number; count: number }>();
|
|
8
|
+
|
|
9
|
+
export function getFileLineCount(filePath: string, size: number, mtimeMs: number): number | undefined {
|
|
10
|
+
const cached = lineCountCache.get(filePath);
|
|
11
|
+
if (cached && cached.size === size && cached.mtimeMs === mtimeMs) {
|
|
12
|
+
return cached.count;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (size > MAX_LINE_COUNT_BYTES) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
try {
|
|
9
20
|
const content = readFileSync(filePath, "utf-8");
|
|
10
|
-
|
|
21
|
+
const count = content.split("\n").length;
|
|
22
|
+
lineCountCache.set(filePath, { mtimeMs, size, count });
|
|
23
|
+
return count;
|
|
11
24
|
} catch {
|
|
12
|
-
return
|
|
25
|
+
return undefined;
|
|
13
26
|
}
|
|
14
27
|
}
|
|
15
28
|
|
|
@@ -98,7 +111,7 @@ export function buildFileTree(
|
|
|
98
111
|
isDirectory: false,
|
|
99
112
|
gitStatus: fileGitStatus,
|
|
100
113
|
agentModified: agentModified.has(fullPath),
|
|
101
|
-
lineCount: getFileLineCount(fullPath),
|
|
114
|
+
lineCount: getFileLineCount(fullPath, stat.size, stat.mtimeMs),
|
|
102
115
|
diffStats: fileDiffStats,
|
|
103
116
|
});
|
|
104
117
|
}
|
|
@@ -110,14 +123,22 @@ export function buildFileTree(
|
|
|
110
123
|
let totalLines = 0;
|
|
111
124
|
let totalAdditions = 0;
|
|
112
125
|
let totalDeletions = 0;
|
|
126
|
+
let lineCountComplete = true;
|
|
113
127
|
|
|
114
128
|
for (const child of node.children) {
|
|
115
129
|
if (child.isDirectory) {
|
|
116
|
-
totalLines += child.totalLines
|
|
117
|
-
totalAdditions += child.totalAdditions
|
|
118
|
-
totalDeletions += child.totalDeletions
|
|
130
|
+
totalLines += child.totalLines ?? 0;
|
|
131
|
+
totalAdditions += child.totalAdditions ?? 0;
|
|
132
|
+
totalDeletions += child.totalDeletions ?? 0;
|
|
133
|
+
if (child.lineCountComplete === false) {
|
|
134
|
+
lineCountComplete = false;
|
|
135
|
+
}
|
|
119
136
|
} else {
|
|
120
|
-
|
|
137
|
+
if (child.lineCount === undefined) {
|
|
138
|
+
lineCountComplete = false;
|
|
139
|
+
} else {
|
|
140
|
+
totalLines += child.lineCount;
|
|
141
|
+
}
|
|
121
142
|
if (child.diffStats) {
|
|
122
143
|
totalAdditions += child.diffStats.additions;
|
|
123
144
|
totalDeletions += child.diffStats.deletions;
|
|
@@ -128,27 +149,33 @@ export function buildFileTree(
|
|
|
128
149
|
node.totalLines = totalLines;
|
|
129
150
|
node.totalAdditions = totalAdditions;
|
|
130
151
|
node.totalDeletions = totalDeletions;
|
|
152
|
+
node.lineCountComplete = lineCountComplete;
|
|
131
153
|
} catch {}
|
|
132
154
|
|
|
133
155
|
return node;
|
|
134
156
|
}
|
|
135
157
|
|
|
136
|
-
export function flattenTree(
|
|
158
|
+
export function flattenTree(
|
|
159
|
+
node: FileNode,
|
|
160
|
+
depth = 0,
|
|
161
|
+
isRoot = true,
|
|
162
|
+
includeCollapsed = false
|
|
163
|
+
): FlatNode[] {
|
|
137
164
|
const result: FlatNode[] = [];
|
|
138
165
|
|
|
139
166
|
// Skip the root "." node itself, just process its children
|
|
140
167
|
if (isRoot && node.name === ".") {
|
|
141
168
|
for (const child of node.children || []) {
|
|
142
|
-
result.push(...flattenTree(child, 0, false));
|
|
169
|
+
result.push(...flattenTree(child, 0, false, includeCollapsed));
|
|
143
170
|
}
|
|
144
171
|
return result;
|
|
145
172
|
}
|
|
146
173
|
|
|
147
174
|
result.push({ node, depth });
|
|
148
175
|
|
|
149
|
-
if (node.isDirectory && node.
|
|
176
|
+
if (node.isDirectory && node.children && (includeCollapsed || node.expanded)) {
|
|
150
177
|
for (const child of node.children) {
|
|
151
|
-
result.push(...flattenTree(child, depth + 1, false));
|
|
178
|
+
result.push(...flattenTree(child, depth + 1, false, includeCollapsed));
|
|
152
179
|
}
|
|
153
180
|
}
|
|
154
181
|
|
package/package.json
CHANGED
package/types.ts
CHANGED
package/viewer.ts
CHANGED
|
@@ -178,11 +178,11 @@ export function createViewer(
|
|
|
178
178
|
if (state.file.diffStats.deletions > 0) {
|
|
179
179
|
header += theme.fg("error", ` -${state.file.diffStats.deletions}`);
|
|
180
180
|
}
|
|
181
|
-
} else if (isUntracked && state.file.lineCount) {
|
|
181
|
+
} else if (isUntracked && state.file.lineCount !== undefined) {
|
|
182
182
|
header += theme.fg("success", ` +${state.file.lineCount}`);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
if (state.file.lineCount) {
|
|
185
|
+
if (state.file.lineCount !== undefined) {
|
|
186
186
|
header += theme.fg("dim", ` ${state.file.lineCount}L`);
|
|
187
187
|
}
|
|
188
188
|
|
|
@@ -245,8 +245,10 @@ export function createViewer(
|
|
|
245
245
|
|
|
246
246
|
try {
|
|
247
247
|
state.rawContent = readFileSync(file.path, "utf-8");
|
|
248
|
+
file.lineCount = state.rawContent.split("\n").length;
|
|
248
249
|
} catch {
|
|
249
250
|
state.rawContent = "";
|
|
251
|
+
file.lineCount = undefined;
|
|
250
252
|
}
|
|
251
253
|
},
|
|
252
254
|
|