@pi-unipi/utility 0.2.7 → 0.2.8
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/package.json +1 -1
- package/src/diff/highlighter.ts +1 -2
- package/src/diff/renderer.ts +25 -52
- package/src/diff/wrapper.ts +60 -8
package/package.json
CHANGED
package/src/diff/highlighter.ts
CHANGED
|
@@ -273,9 +273,8 @@ export async function getShikiHighlighter(): Promise<any> {
|
|
|
273
273
|
],
|
|
274
274
|
});
|
|
275
275
|
return shikiHighlighter;
|
|
276
|
-
} catch
|
|
276
|
+
} catch {
|
|
277
277
|
// If Shiki fails to load, return null — we'll use plain text
|
|
278
|
-
console.warn("[pi-diff] Shiki highlighter failed to load:", err);
|
|
279
278
|
shikiInitPromise = null;
|
|
280
279
|
return null;
|
|
281
280
|
}
|
package/src/diff/renderer.ts
CHANGED
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ANSI diff rendering: split (side-by-side) and unified (stacked) views.
|
|
5
5
|
* Includes ANSI utilities, background injection, and adaptive wrapping.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: All output lines must be truncated to terminal width
|
|
8
|
+
* to prevent TUI crashes. Use fit() or truncateToTermWidth()
|
|
9
|
+
* before pushing lines.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import type { ParsedDiff, DiffLine } from "./parser.js";
|
|
9
13
|
import type { DiffColors } from "./theme.js";
|
|
10
14
|
import { hexToBgAnsi, hexToFgAnsi } from "./theme.js";
|
|
11
15
|
import { hlBlock, detectLanguage } from "./highlighter.js";
|
|
16
|
+
import { truncateToWidth as piTruncateToWidth, visibleWidth as piVisibleWidth } from "@mariozechner/pi-tui";
|
|
12
17
|
|
|
13
18
|
// ─── Constants ──────────────────────────────────────────────────────────────────
|
|
14
19
|
|
|
@@ -36,64 +41,21 @@ export function strip(s: string): string {
|
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
43
|
* Get the visible width of a string (excluding ANSI escapes).
|
|
39
|
-
*
|
|
44
|
+
* Delegates to pi-tui's ANSI-aware implementation for correctness.
|
|
40
45
|
*/
|
|
41
46
|
export function visibleWidth(s: string): number {
|
|
42
|
-
|
|
43
|
-
let width = 0;
|
|
44
|
-
for (const char of stripped) {
|
|
45
|
-
const code = char.codePointAt(0)!;
|
|
46
|
-
// CJK Unified Ideographs, CJK Compatibility, etc.
|
|
47
|
-
if (
|
|
48
|
-
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
49
|
-
(code >= 0x3000 && code <= 0x30ff) ||
|
|
50
|
-
(code >= 0xff00 && code <= 0xffef) ||
|
|
51
|
-
(code >= 0xf900 && code <= 0xfaff) ||
|
|
52
|
-
(code >= 0x2e80 && code <= 0x2eff) ||
|
|
53
|
-
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
54
|
-
(code >= 0x20000 && code <= 0x2a6df)
|
|
55
|
-
) {
|
|
56
|
-
width += 2;
|
|
57
|
-
} else if (code > 0xffff) {
|
|
58
|
-
// Surrogate pairs / emoji — typically width 2
|
|
59
|
-
width += 2;
|
|
60
|
-
} else {
|
|
61
|
-
width += 1;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return width;
|
|
47
|
+
return piVisibleWidth(s);
|
|
65
48
|
}
|
|
66
49
|
|
|
67
50
|
/**
|
|
68
51
|
* Fit a string to a target width, padding or truncating as needed.
|
|
69
|
-
*
|
|
52
|
+
* Uses pi-tui's ANSI-aware truncation for correctness.
|
|
70
53
|
*/
|
|
71
54
|
export function fit(s: string, targetWidth: number): string {
|
|
72
55
|
const vw = visibleWidth(s);
|
|
73
56
|
if (vw === targetWidth) return s;
|
|
74
57
|
if (vw > targetWidth) {
|
|
75
|
-
|
|
76
|
-
let width = 0;
|
|
77
|
-
let i = 0;
|
|
78
|
-
const stripped = strip(s);
|
|
79
|
-
for (; i < stripped.length && width < targetWidth; i++) {
|
|
80
|
-
const code = stripped.codePointAt(i)!;
|
|
81
|
-
width += (code >= 0x4e00 && code <= 0x9fff) || code > 0xffff ? 2 : 1;
|
|
82
|
-
}
|
|
83
|
-
// Find the corresponding position in the original (with ANSI) string
|
|
84
|
-
let strippedIdx = 0;
|
|
85
|
-
let origIdx = 0;
|
|
86
|
-
while (origIdx < s.length && strippedIdx < i) {
|
|
87
|
-
if (s[origIdx] === "\x1b") {
|
|
88
|
-
// Skip ANSI sequence
|
|
89
|
-
while (origIdx < s.length && s[origIdx] !== "m") origIdx++;
|
|
90
|
-
origIdx++;
|
|
91
|
-
} else {
|
|
92
|
-
strippedIdx++;
|
|
93
|
-
origIdx++;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return s.substring(0, origIdx) + "\x1b[0m";
|
|
58
|
+
return piTruncateToWidth(s, targetWidth, "");
|
|
97
59
|
}
|
|
98
60
|
// Pad
|
|
99
61
|
return s + " ".repeat(targetWidth - vw);
|
|
@@ -198,6 +160,17 @@ export function termW(): number {
|
|
|
198
160
|
}
|
|
199
161
|
}
|
|
200
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Truncate a line to terminal width using pi-tui's ANSI-aware truncation.
|
|
165
|
+
* Prevents TUI crash when rendered lines exceed terminal width.
|
|
166
|
+
* Accounts for rendering overhead from Box nesting (~6 chars).
|
|
167
|
+
*/
|
|
168
|
+
export function truncateToTermWidth(line: string): string {
|
|
169
|
+
const maxW = Math.max(20, termW() - 6);
|
|
170
|
+
if (piVisibleWidth(line) <= maxW) return line;
|
|
171
|
+
return piTruncateToWidth(line, maxW, "…");
|
|
172
|
+
}
|
|
173
|
+
|
|
201
174
|
// ─── Background Injection ───────────────────────────────────────────────────────
|
|
202
175
|
|
|
203
176
|
/**
|
|
@@ -313,16 +286,16 @@ export function renderUnified(
|
|
|
313
286
|
|
|
314
287
|
switch (line.type) {
|
|
315
288
|
case "hunk":
|
|
316
|
-
lines.push(`${hunkFg}${line.content}${reset}`);
|
|
289
|
+
lines.push(truncateToTermWidth(`${hunkFg}${line.content}${reset}`));
|
|
317
290
|
break;
|
|
318
291
|
case "add":
|
|
319
|
-
lines.push(`${addBg}${addFg}+${reset}${addBg} ${lnum(null, 4)} ${lnum(line.newLine, 4)} │ ${line.content}${reset}`);
|
|
292
|
+
lines.push(truncateToTermWidth(`${addBg}${addFg}+${reset}${addBg} ${lnum(null, 4)} ${lnum(line.newLine, 4)} │ ${line.content}${reset}`));
|
|
320
293
|
break;
|
|
321
294
|
case "remove":
|
|
322
|
-
lines.push(`${remBg}${remFg}-${reset}${remBg} ${lnum(line.oldLine, 4)} ${lnum(null, 4)} │ ${line.content}${reset}`);
|
|
295
|
+
lines.push(truncateToTermWidth(`${remBg}${remFg}-${reset}${remBg} ${lnum(line.oldLine, 4)} ${lnum(null, 4)} │ ${line.content}${reset}`));
|
|
323
296
|
break;
|
|
324
297
|
case "context":
|
|
325
|
-
lines.push(` ${headerFg}${lnum(line.oldLine, 4)} ${lnum(line.newLine, 4)}${reset} │ ${line.content}`);
|
|
298
|
+
lines.push(truncateToTermWidth(` ${headerFg}${lnum(line.oldLine, 4)} ${lnum(line.newLine, 4)}${reset} │ ${line.content}`));
|
|
326
299
|
break;
|
|
327
300
|
}
|
|
328
301
|
}
|
|
@@ -411,7 +384,7 @@ export function renderSplit(
|
|
|
411
384
|
for (let i = 0; i < leftLines.length; i++) {
|
|
412
385
|
const left = leftLines[i];
|
|
413
386
|
const right = rightLines[i];
|
|
414
|
-
lines.push(`${left} │ ${right}`);
|
|
387
|
+
lines.push(truncateToTermWidth(`${left} │ ${right}`));
|
|
415
388
|
}
|
|
416
389
|
|
|
417
390
|
if (truncated) {
|
package/src/diff/wrapper.ts
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import type { ExtensionAPI, ToolDefinition, AgentToolResult } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import { visibleWidth as piVisibleWidth, truncateToWidth as piTruncateToWidth } from "@mariozechner/pi-tui";
|
|
15
16
|
import { readDiffSettings } from "./settings.js";
|
|
16
17
|
import { parseDiff } from "./parser.js";
|
|
17
18
|
import { resolveDiffColors, applyDiffPalette } from "./theme.js";
|
|
18
|
-
import { renderSplit, renderUnified, termW, SPLIT_MIN_WIDTH } from "./renderer.js";
|
|
19
|
+
import { renderSplit, renderUnified, termW, SPLIT_MIN_WIDTH, truncateToTermWidth } from "./renderer.js";
|
|
19
20
|
import { detectLanguageFromPath, hlBlock, MAX_HL_CHARS } from "./highlighter.js";
|
|
20
21
|
|
|
21
22
|
// ─── Types ──────────────────────────────────────────────────────────────────────
|
|
@@ -157,7 +158,15 @@ export function registerEnhancedWriteTool(pi: ExtensionAPI, cwd: string): void {
|
|
|
157
158
|
renderResult(result: any, _options: any, theme: any): any {
|
|
158
159
|
const details = result?.details as DiffToolDetails | undefined;
|
|
159
160
|
if (!details || !details.diff || !details.diff.lines || details.diff.lines.length === 0) {
|
|
160
|
-
|
|
161
|
+
// Error or empty-diff case: render the message from result.content so the
|
|
162
|
+
// user sees "Could not find text to replace..." etc. Never return null here
|
|
163
|
+
// because Container.render() will crash on null child.
|
|
164
|
+
const msg = result?.content?.[0]?.text ?? "";
|
|
165
|
+
return {
|
|
166
|
+
setText: () => {},
|
|
167
|
+
text: msg,
|
|
168
|
+
render: (width: number) => (width > 0 ? [msg.slice(0, width)] : [msg]),
|
|
169
|
+
} as any;
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
try {
|
|
@@ -169,14 +178,34 @@ export function registerEnhancedWriteTool(pi: ExtensionAPI, cwd: string): void {
|
|
|
169
178
|
? renderSplit(details.diff, details.language, max, dc)
|
|
170
179
|
: renderUnified(details.diff, details.language, max, dc);
|
|
171
180
|
|
|
172
|
-
//
|
|
181
|
+
// Split into lines and cache for width-aware rendering.
|
|
182
|
+
// Each line is already truncated to terminal width by
|
|
183
|
+
// truncateToTermWidth() in the renderer, but we also
|
|
184
|
+
// respect the width parameter from Box.render().
|
|
185
|
+
const cachedLines = rendered.split("\n");
|
|
186
|
+
|
|
173
187
|
return {
|
|
174
188
|
setText: () => {},
|
|
175
189
|
text: rendered,
|
|
176
|
-
render: () =>
|
|
190
|
+
render: (width: number) => {
|
|
191
|
+
// If width is provided, re-truncate lines that
|
|
192
|
+
// still exceed it (e.g., inside nested Boxes)
|
|
193
|
+
const maxW = width > 0 ? width : tw;
|
|
194
|
+
return cachedLines.map((line: string) => {
|
|
195
|
+
if (piVisibleWidth(line) > maxW) {
|
|
196
|
+
return piTruncateToWidth(line, maxW, "…");
|
|
197
|
+
}
|
|
198
|
+
return line;
|
|
199
|
+
});
|
|
200
|
+
},
|
|
177
201
|
} as any;
|
|
178
202
|
} catch {
|
|
179
|
-
|
|
203
|
+
const fallback = "(diff rendering failed)";
|
|
204
|
+
return {
|
|
205
|
+
setText: () => {},
|
|
206
|
+
text: fallback,
|
|
207
|
+
render: (width: number) => (width > 0 ? [fallback.slice(0, width)] : [fallback]),
|
|
208
|
+
} as any;
|
|
180
209
|
}
|
|
181
210
|
},
|
|
182
211
|
});
|
|
@@ -262,7 +291,15 @@ export function registerEnhancedEditTool(pi: ExtensionAPI, cwd: string): void {
|
|
|
262
291
|
renderResult(result: any, _options: any, theme: any): any {
|
|
263
292
|
const details = result?.details as DiffToolDetails | undefined;
|
|
264
293
|
if (!details || !details.diff || !details.diff.lines || details.diff.lines.length === 0) {
|
|
265
|
-
|
|
294
|
+
// Error or empty-diff case: render the message from result.content so the
|
|
295
|
+
// user sees "Could not find text to replace..." etc. Never return null here
|
|
296
|
+
// because Container.render() will crash on null child.
|
|
297
|
+
const msg = result?.content?.[0]?.text ?? "";
|
|
298
|
+
return {
|
|
299
|
+
setText: () => {},
|
|
300
|
+
text: msg,
|
|
301
|
+
render: (width: number) => (width > 0 ? [msg.slice(0, width)] : [msg]),
|
|
302
|
+
} as any;
|
|
266
303
|
}
|
|
267
304
|
|
|
268
305
|
try {
|
|
@@ -274,13 +311,28 @@ export function registerEnhancedEditTool(pi: ExtensionAPI, cwd: string): void {
|
|
|
274
311
|
? renderSplit(details.diff, details.language, max, dc)
|
|
275
312
|
: renderUnified(details.diff, details.language, max, dc);
|
|
276
313
|
|
|
314
|
+
const cachedLines = rendered.split("\n");
|
|
315
|
+
|
|
277
316
|
return {
|
|
278
317
|
setText: () => {},
|
|
279
318
|
text: rendered,
|
|
280
|
-
render: () =>
|
|
319
|
+
render: (width: number) => {
|
|
320
|
+
const maxW = width > 0 ? width : tw;
|
|
321
|
+
return cachedLines.map((line: string) => {
|
|
322
|
+
if (piVisibleWidth(line) > maxW) {
|
|
323
|
+
return piTruncateToWidth(line, maxW, "…");
|
|
324
|
+
}
|
|
325
|
+
return line;
|
|
326
|
+
});
|
|
327
|
+
},
|
|
281
328
|
} as any;
|
|
282
329
|
} catch {
|
|
283
|
-
|
|
330
|
+
const fallback = "(diff rendering failed)";
|
|
331
|
+
return {
|
|
332
|
+
setText: () => {},
|
|
333
|
+
text: fallback,
|
|
334
|
+
render: (width: number) => (width > 0 ? [fallback.slice(0, width)] : [fallback]),
|
|
335
|
+
} as any;
|
|
284
336
|
}
|
|
285
337
|
},
|
|
286
338
|
});
|