@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 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 computeTotalStats(root: FileNode | null): { totalLines: number; additions: number; deletions: number } {
104
- let totalLines = 0;
105
- let additions = 0;
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
- if (root) traverse(root);
124
- return { totalLines, additions, deletions };
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: buildFileTree(cwd, cwd, gitStatus, diffStats, ignored, agentModifiedFiles),
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 currentPath = browser.flatList[browser.selectedIndex]?.node.path;
220
- const viewingFilePath = viewer.getFile()?.path;
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 = browser.flatList.findIndex(f => f.node.path === currentPath);
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, browser.flatList.length - 1));
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 = computeTotalStats(browser.root);
332
+ const stats = browser.stats;
325
333
 
326
- let statsDisplay = theme.fg("dim", ` ${stats.totalLines}L`);
327
- if (stats.additions > 0 || stats.deletions > 0) {
328
- if (stats.additions > 0) statsDisplay += theme.fg("success", ` +${stats.additions}`);
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
@@ -1,5 +1,6 @@
1
1
  export const MAX_TREE_DEPTH = 6;
2
2
  export const POLL_INTERVAL_MS = 3000;
3
+ export const MAX_LINE_COUNT_BYTES = 256 * 1024;
3
4
 
4
5
  export const DEFAULT_VIEWER_HEIGHT = 18;
5
6
  export const DEFAULT_BROWSER_HEIGHT = 15;
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
- export function getFileLineCount(filePath: string): number {
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
- return content.split("\n").length;
21
+ const count = content.split("\n").length;
22
+ lineCountCache.set(filePath, { mtimeMs, size, count });
23
+ return count;
11
24
  } catch {
12
- return 0;
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 || 0;
117
- totalAdditions += child.totalAdditions || 0;
118
- totalDeletions += child.totalDeletions || 0;
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
- totalLines += child.lineCount || 0;
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(node: FileNode, depth = 0, isRoot = true): FlatNode[] {
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.expanded && node.children) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-files-widget",
3
- "version": "0.1.0",
3
+ "version": "0.1.7",
4
4
  "description": "In-terminal file browser and viewer for Pi.",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Mustier",
package/types.ts CHANGED
@@ -18,6 +18,7 @@ export interface FileNode {
18
18
  totalLines?: number;
19
19
  totalAdditions?: number;
20
20
  totalDeletions?: number;
21
+ lineCountComplete?: boolean;
21
22
  }
22
23
 
23
24
  export interface FlatNode {
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