@heyhuynhgiabuu/pi-diff 0.1.1 → 0.1.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/README.md +75 -1
- package/biome.json +20 -15
- package/package.json +50 -50
- package/src/index.ts +642 -175
package/src/index.ts
CHANGED
|
@@ -28,11 +28,292 @@ import { codeToANSI } from "@shikijs/cli";
|
|
|
28
28
|
import * as Diff from "diff";
|
|
29
29
|
import type { BundledLanguage, BundledTheme } from "shiki";
|
|
30
30
|
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Diff Theme System — presets, auto-derive, and per-color overrides
|
|
33
|
+
//
|
|
34
|
+
// Resolution chain (per color, highest priority first):
|
|
35
|
+
// 1. Environment variable override (e.g. DIFF_BG_ADD="#1a3320")
|
|
36
|
+
// 2. diffColors.bgAdd from .pi/settings.json (explicit per-color hex)
|
|
37
|
+
// 3. diffTheme preset value (named preset like "midnight")
|
|
38
|
+
// 4. Auto-derived from pi theme fg colors (default behavior)
|
|
39
|
+
// 5. Hardcoded fallback
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/** Hex color palette for a diff theme preset. All values "#RRGGBB". */
|
|
43
|
+
interface DiffPreset {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
shikiTheme?: string;
|
|
47
|
+
bgAdd?: string;
|
|
48
|
+
bgDel?: string;
|
|
49
|
+
bgAddHighlight?: string;
|
|
50
|
+
bgDelHighlight?: string;
|
|
51
|
+
bgGutterAdd?: string;
|
|
52
|
+
bgGutterDel?: string;
|
|
53
|
+
bgEmpty?: string;
|
|
54
|
+
fgAdd?: string;
|
|
55
|
+
fgDel?: string;
|
|
56
|
+
fgDim?: string;
|
|
57
|
+
fgLnum?: string;
|
|
58
|
+
fgRule?: string;
|
|
59
|
+
fgStripe?: string;
|
|
60
|
+
fgSafeMuted?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** User diff config read from .pi/settings.json */
|
|
64
|
+
interface DiffUserConfig {
|
|
65
|
+
diffTheme?: string;
|
|
66
|
+
diffColors?: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DIFF_PRESETS: Record<string, DiffPreset> = {
|
|
70
|
+
default: {
|
|
71
|
+
name: "default",
|
|
72
|
+
description: "Original pi-diff colors — tuned for dark theme bases (~#1e1e2e)",
|
|
73
|
+
bgAdd: "#162620",
|
|
74
|
+
bgDel: "#2d1919",
|
|
75
|
+
bgAddHighlight: "#234b32",
|
|
76
|
+
bgDelHighlight: "#502323",
|
|
77
|
+
bgGutterAdd: "#12201a",
|
|
78
|
+
bgGutterDel: "#261616",
|
|
79
|
+
bgEmpty: "#121212",
|
|
80
|
+
fgDim: "#505050",
|
|
81
|
+
fgLnum: "#646464",
|
|
82
|
+
fgRule: "#323232",
|
|
83
|
+
fgStripe: "#282828",
|
|
84
|
+
fgSafeMuted: "#8b949e",
|
|
85
|
+
},
|
|
86
|
+
midnight: {
|
|
87
|
+
name: "midnight",
|
|
88
|
+
description: "Subtle tints for pure black (#000000) terminal backgrounds",
|
|
89
|
+
bgAdd: "#0d1a12",
|
|
90
|
+
bgDel: "#1a0d0d",
|
|
91
|
+
bgAddHighlight: "#1a3825",
|
|
92
|
+
bgDelHighlight: "#381a1a",
|
|
93
|
+
bgGutterAdd: "#091208",
|
|
94
|
+
bgGutterDel: "#120908",
|
|
95
|
+
bgEmpty: "#080808",
|
|
96
|
+
fgDim: "#404040",
|
|
97
|
+
fgLnum: "#505050",
|
|
98
|
+
fgRule: "#282828",
|
|
99
|
+
fgStripe: "#1e1e1e",
|
|
100
|
+
fgSafeMuted: "#8b949e",
|
|
101
|
+
},
|
|
102
|
+
subtle: {
|
|
103
|
+
name: "subtle",
|
|
104
|
+
description: "Minimal backgrounds — barely-there tints for a clean look",
|
|
105
|
+
bgAdd: "#081008",
|
|
106
|
+
bgDel: "#100808",
|
|
107
|
+
bgAddHighlight: "#122818",
|
|
108
|
+
bgDelHighlight: "#281212",
|
|
109
|
+
bgGutterAdd: "#060c06",
|
|
110
|
+
bgGutterDel: "#0c0606",
|
|
111
|
+
bgEmpty: "#060606",
|
|
112
|
+
fgDim: "#383838",
|
|
113
|
+
fgLnum: "#484848",
|
|
114
|
+
fgRule: "#242424",
|
|
115
|
+
fgStripe: "#181818",
|
|
116
|
+
fgSafeMuted: "#8b949e",
|
|
117
|
+
},
|
|
118
|
+
neon: {
|
|
119
|
+
name: "neon",
|
|
120
|
+
description: "Higher contrast backgrounds for better visibility",
|
|
121
|
+
bgAdd: "#1a3320",
|
|
122
|
+
bgDel: "#331a16",
|
|
123
|
+
bgAddHighlight: "#2d5c3a",
|
|
124
|
+
bgDelHighlight: "#5c2d2d",
|
|
125
|
+
bgGutterAdd: "#142818",
|
|
126
|
+
bgGutterDel: "#28120e",
|
|
127
|
+
bgEmpty: "#141414",
|
|
128
|
+
fgDim: "#606060",
|
|
129
|
+
fgLnum: "#787878",
|
|
130
|
+
fgRule: "#404040",
|
|
131
|
+
fgStripe: "#303030",
|
|
132
|
+
fgSafeMuted: "#9da5ae",
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** Parse 24-bit ANSI color code → RGB. Works for both fg and bg escapes. */
|
|
137
|
+
function parseAnsiRgb(ansi: string): { r: number; g: number; b: number } | null {
|
|
138
|
+
const esc = "\u001b";
|
|
139
|
+
const m = ansi.match(new RegExp(`${esc}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`));
|
|
140
|
+
return m ? { r: +m[1], g: +m[2], b: +m[3] } : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Convert "#RRGGBB" hex → ANSI 24-bit background escape. */
|
|
144
|
+
function hexToBgAnsi(hex: string): string {
|
|
145
|
+
if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return "";
|
|
146
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
147
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
148
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
149
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Convert "#RRGGBB" hex → ANSI 24-bit foreground escape. */
|
|
153
|
+
function hexToFgAnsi(hex: string): string {
|
|
154
|
+
if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return "";
|
|
155
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
156
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
157
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
158
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Derive a muted background ANSI code from a foreground ANSI code.
|
|
162
|
+
* Scales the fg RGB by `intensity` (0.0–1.0) to produce a subtle tint. */
|
|
163
|
+
function deriveBgFromFg(fgAnsi: string, intensity: number): string {
|
|
164
|
+
const rgb = parseAnsiRgb(fgAnsi);
|
|
165
|
+
if (!rgb) return "";
|
|
166
|
+
const r = Math.round(rgb.r * intensity);
|
|
167
|
+
const g = Math.round(rgb.g * intensity);
|
|
168
|
+
const b = Math.round(rgb.b * intensity);
|
|
169
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Mix an accent color into a base color at the given intensity (0.0–1.0).
|
|
173
|
+
* Returns an ANSI 24-bit background escape. Used to derive diff backgrounds
|
|
174
|
+
* that blend with the tool box background (toolSuccessBg). */
|
|
175
|
+
function mixBg(base: { r: number; g: number; b: number }, accent: { r: number; g: number; b: number }, intensity: number): string {
|
|
176
|
+
const r = Math.round(base.r + (accent.r - base.r) * intensity);
|
|
177
|
+
const g = Math.round(base.g + (accent.g - base.g) * intensity);
|
|
178
|
+
const b = Math.round(base.b + (accent.b - base.b) * intensity);
|
|
179
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether auto-derive from theme is still pending (runs lazily on first render). */
|
|
183
|
+
let _autoDerivePending = true;
|
|
184
|
+
|
|
185
|
+
/** Whether user set explicit bg config (via preset or per-color overrides). */
|
|
186
|
+
let _hasExplicitBgConfig = false;
|
|
187
|
+
|
|
188
|
+
/** Auto-derive all diff background colors from the pi theme's fg diff colors.
|
|
189
|
+
* Reads toolSuccessBg as the base and mixes accent colors into it.
|
|
190
|
+
* Falls back to black (0,0,0) as base if toolSuccessBg is unavailable. */
|
|
191
|
+
function autoDeriveBgFromTheme(theme: any): void {
|
|
192
|
+
if (!theme?.getFgAnsi) return;
|
|
193
|
+
try {
|
|
194
|
+
const fgAdd = theme.getFgAnsi("toolDiffAdded");
|
|
195
|
+
const fgDel = theme.getFgAnsi("toolDiffRemoved");
|
|
196
|
+
const addRgb = parseAnsiRgb(fgAdd);
|
|
197
|
+
const delRgb = parseAnsiRgb(fgDel);
|
|
198
|
+
if (!addRgb || !delRgb) return;
|
|
199
|
+
|
|
200
|
+
// Read toolSuccessBg as the base background color
|
|
201
|
+
let base = { r: 0, g: 0, b: 0 };
|
|
202
|
+
if (theme.getBgAnsi) {
|
|
203
|
+
try {
|
|
204
|
+
const bgAnsi = theme.getBgAnsi("toolSuccessBg");
|
|
205
|
+
const parsed = parseAnsiRgb(bgAnsi);
|
|
206
|
+
if (parsed) {
|
|
207
|
+
base = parsed;
|
|
208
|
+
BG_BASE = bgAnsi;
|
|
209
|
+
}
|
|
210
|
+
} catch { /* no toolSuccessBg — use black */ }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Line backgrounds — subtle accent mixed into base (8–10%)
|
|
214
|
+
BG_ADD = mixBg(base, addRgb, 0.08);
|
|
215
|
+
BG_DEL = mixBg(base, delRgb, 0.10);
|
|
216
|
+
|
|
217
|
+
// Word-level highlights — more visible (20–22%)
|
|
218
|
+
BG_ADD_W = mixBg(base, addRgb, 0.20);
|
|
219
|
+
BG_DEL_W = mixBg(base, delRgb, 0.22);
|
|
220
|
+
|
|
221
|
+
// Gutters — subtler than lines (5–6%)
|
|
222
|
+
BG_GUTTER_ADD = mixBg(base, addRgb, 0.05);
|
|
223
|
+
BG_GUTTER_DEL = mixBg(base, delRgb, 0.06);
|
|
224
|
+
|
|
225
|
+
// Empty filler and context — match the base
|
|
226
|
+
BG_EMPTY = BG_BASE;
|
|
227
|
+
|
|
228
|
+
// Rebuild derived constants
|
|
229
|
+
DIVIDER = `${FG_RULE}│${RST}`;
|
|
230
|
+
} catch {
|
|
231
|
+
// Fall back to defaults silently
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Load diff theme config from .pi/settings.json (project-level, then global). */
|
|
236
|
+
function loadDiffConfig(): DiffUserConfig {
|
|
237
|
+
const paths = [
|
|
238
|
+
`${process.cwd()}/.pi/settings.json`,
|
|
239
|
+
`${process.env.HOME ?? ""}/.pi/settings.json`,
|
|
240
|
+
];
|
|
241
|
+
for (const p of paths) {
|
|
242
|
+
try {
|
|
243
|
+
if (existsSync(p)) {
|
|
244
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
245
|
+
if (raw.diffTheme || raw.diffColors) {
|
|
246
|
+
return { diffTheme: raw.diffTheme, diffColors: raw.diffColors };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// skip invalid files
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return {};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Apply diff palette from settings → preset → (auto-derive deferred) → defaults.
|
|
257
|
+
* Called once during extension initialization. */
|
|
258
|
+
function applyDiffPalette(): void {
|
|
259
|
+
const config = loadDiffConfig();
|
|
260
|
+
|
|
261
|
+
// Load preset if specified
|
|
262
|
+
const preset = config.diffTheme ? DIFF_PRESETS[config.diffTheme] : null;
|
|
263
|
+
if (preset) _hasExplicitBgConfig = true;
|
|
264
|
+
|
|
265
|
+
// Per-color overrides from settings
|
|
266
|
+
const ov = config.diffColors ?? {};
|
|
267
|
+
if (Object.keys(ov).length > 0) _hasExplicitBgConfig = true;
|
|
268
|
+
|
|
269
|
+
// Helper: apply a hex bg color if not env-overridden
|
|
270
|
+
const applyBg = (envName: string | null, key: string, presetVal: string | undefined, set: (v: string) => void) => {
|
|
271
|
+
if (envName && process.env[envName]) return; // env override wins
|
|
272
|
+
const hex = ov[key] ?? presetVal;
|
|
273
|
+
if (hex) { const a = hexToBgAnsi(hex); if (a) set(a); }
|
|
274
|
+
};
|
|
275
|
+
// Helper: apply a hex fg color if not env-overridden
|
|
276
|
+
const applyFg = (envName: string | null, key: string, presetVal: string | undefined, set: (v: string) => void) => {
|
|
277
|
+
if (envName && process.env[envName]) return;
|
|
278
|
+
const hex = ov[key] ?? presetVal;
|
|
279
|
+
if (hex) { const a = hexToFgAnsi(hex); if (a) set(a); }
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// --- Apply backgrounds ---
|
|
283
|
+
applyBg("DIFF_BG_ADD", "bgAdd", preset?.bgAdd, (v) => { BG_ADD = v; });
|
|
284
|
+
applyBg("DIFF_BG_DEL", "bgDel", preset?.bgDel, (v) => { BG_DEL = v; });
|
|
285
|
+
applyBg("DIFF_BG_ADD_HL", "bgAddHighlight", preset?.bgAddHighlight, (v) => { BG_ADD_W = v; });
|
|
286
|
+
applyBg("DIFF_BG_DEL_HL", "bgDelHighlight", preset?.bgDelHighlight, (v) => { BG_DEL_W = v; });
|
|
287
|
+
applyBg("DIFF_BG_GUTTER_ADD", "bgGutterAdd", preset?.bgGutterAdd, (v) => { BG_GUTTER_ADD = v; });
|
|
288
|
+
applyBg("DIFF_BG_GUTTER_DEL", "bgGutterDel", preset?.bgGutterDel, (v) => { BG_GUTTER_DEL = v; });
|
|
289
|
+
applyBg(null, "bgEmpty", preset?.bgEmpty, (v) => { BG_EMPTY = v; });
|
|
290
|
+
|
|
291
|
+
// --- Apply foregrounds ---
|
|
292
|
+
applyFg("DIFF_FG_ADD", "fgAdd", preset?.fgAdd, (v) => { FG_ADD = v; });
|
|
293
|
+
applyFg("DIFF_FG_DEL", "fgDel", preset?.fgDel, (v) => { FG_DEL = v; });
|
|
294
|
+
applyFg(null, "fgDim", preset?.fgDim, (v) => { FG_DIM = v; });
|
|
295
|
+
applyFg(null, "fgLnum", preset?.fgLnum, (v) => { FG_LNUM = v; });
|
|
296
|
+
applyFg(null, "fgRule", preset?.fgRule, (v) => { FG_RULE = v; });
|
|
297
|
+
applyFg(null, "fgStripe", preset?.fgStripe, (v) => { FG_STRIPE = v; });
|
|
298
|
+
applyFg(null, "fgSafeMuted", preset?.fgSafeMuted, (v) => { FG_SAFE_MUTED = v; });
|
|
299
|
+
|
|
300
|
+
// --- Shiki syntax theme ---
|
|
301
|
+
const shiki = ov.shikiTheme ?? preset?.shikiTheme;
|
|
302
|
+
if (shiki) THEME = shiki as BundledTheme;
|
|
303
|
+
|
|
304
|
+
// --- Rebuild derived constants ---
|
|
305
|
+
DIVIDER = `${FG_RULE}│${RST}`;
|
|
306
|
+
DEFAULT_DIFF_COLORS = { fgAdd: FG_ADD, fgDel: FG_DEL, fgCtx: FG_DIM };
|
|
307
|
+
|
|
308
|
+
// If no explicit bg config, auto-derive will run on first render
|
|
309
|
+
_autoDerivePending = !_hasExplicitBgConfig;
|
|
310
|
+
}
|
|
311
|
+
|
|
31
312
|
// ---------------------------------------------------------------------------
|
|
32
313
|
// Config
|
|
33
314
|
// ---------------------------------------------------------------------------
|
|
34
315
|
|
|
35
|
-
|
|
316
|
+
let THEME: BundledTheme = (process.env.DIFF_THEME as BundledTheme | undefined) ?? "github-dark";
|
|
36
317
|
|
|
37
318
|
function envInt(name: string, fallback: number): number {
|
|
38
319
|
const v = Number.parseInt(process.env[name] ?? "", 10);
|
|
@@ -60,30 +341,30 @@ function envBg(name: string, fallback: string): string {
|
|
|
60
341
|
// --- Split-view thresholds ---
|
|
61
342
|
// Split is preferred when there's real room. At narrow widths, a clean stacked
|
|
62
343
|
// (unified) view is better than a cramped split with wrapping.
|
|
63
|
-
const SPLIT_MIN_WIDTH = envInt("DIFF_SPLIT_MIN_WIDTH", 150);
|
|
344
|
+
const SPLIT_MIN_WIDTH = envInt("DIFF_SPLIT_MIN_WIDTH", 150); // need ≥150 cols for split to breathe
|
|
64
345
|
const SPLIT_MIN_CODE_WIDTH = envInt("DIFF_SPLIT_MIN_CODE_WIDTH", 60); // ≥60 code cols per side
|
|
65
|
-
const SPLIT_MAX_WRAP_RATIO = 0.
|
|
66
|
-
const SPLIT_MAX_WRAP_LINES = 8;
|
|
346
|
+
const SPLIT_MAX_WRAP_RATIO = 0.2; // if >20% lines wrap in split, fall back to stacked
|
|
347
|
+
const SPLIT_MAX_WRAP_LINES = 8; // absolute cap before unified fallback
|
|
67
348
|
|
|
68
349
|
// --- Terminal bounds ---
|
|
69
|
-
const MAX_TERM_WIDTH = 210;
|
|
70
|
-
const DEFAULT_TERM_WIDTH = 200;
|
|
350
|
+
const MAX_TERM_WIDTH = 210; // max for 1728px wide display (~205 cols at typical font)
|
|
351
|
+
const DEFAULT_TERM_WIDTH = 200; // safe default for 1728x1117 resolution
|
|
71
352
|
|
|
72
353
|
// --- Rendering limits ---
|
|
73
|
-
const MAX_PREVIEW_LINES = 60;
|
|
74
|
-
const MAX_RENDER_LINES = 150;
|
|
75
|
-
const MAX_HL_CHARS = 80_000;
|
|
76
|
-
const CACHE_LIMIT = 192;
|
|
354
|
+
const MAX_PREVIEW_LINES = 60; // was 50 — show slightly more context in edit preview
|
|
355
|
+
const MAX_RENDER_LINES = 150; // was 120 — show more of the diff in write tool
|
|
356
|
+
const MAX_HL_CHARS = 80_000; // was 50k — allow syntax hl for larger diffs
|
|
357
|
+
const CACHE_LIMIT = 192; // was 128 — bigger cache for multi-file sessions
|
|
77
358
|
|
|
78
359
|
// --- Word diff ---
|
|
79
|
-
const WORD_DIFF_MIN_SIM = 0.15;
|
|
360
|
+
const WORD_DIFF_MIN_SIM = 0.15; // was 0.2 — show word diffs for slightly less similar lines
|
|
80
361
|
|
|
81
362
|
// --- Wrapping ---
|
|
82
363
|
// Adaptive: narrow terminals truncate aggressively, wide terminals allow wrapping.
|
|
83
364
|
// Actual wrap rows are computed per-render via adaptiveWrapRows().
|
|
84
|
-
const MAX_WRAP_ROWS_WIDE = 3;
|
|
85
|
-
const MAX_WRAP_ROWS_MED
|
|
86
|
-
const MAX_WRAP_ROWS_NARROW = 1;
|
|
365
|
+
const MAX_WRAP_ROWS_WIDE = 3; // ≥180 cols
|
|
366
|
+
const MAX_WRAP_ROWS_MED = 2; // 120–179 cols
|
|
367
|
+
const MAX_WRAP_ROWS_NARROW = 1; // <120 cols (truncate, no wrap)
|
|
87
368
|
|
|
88
369
|
// ---------------------------------------------------------------------------
|
|
89
370
|
// ANSI
|
|
@@ -95,24 +376,24 @@ const DIM = "\x1b[2m";
|
|
|
95
376
|
|
|
96
377
|
// Subtle diff backgrounds — muted tones to let syntax fg shine through
|
|
97
378
|
// Override via env: DIFF_BG_ADD="#1a3320" etc. (hex "#RRGGBB" format)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const BG_GUTTER_CTX = "";
|
|
105
|
-
|
|
379
|
+
let BG_ADD = envBg("DIFF_BG_ADD", "\x1b[48;2;22;38;32m"); // muted teal-green
|
|
380
|
+
let BG_DEL = envBg("DIFF_BG_DEL", "\x1b[48;2;45;25;25m"); // muted brown-red
|
|
381
|
+
let BG_ADD_W = envBg("DIFF_BG_ADD_HL", "\x1b[48;2;35;75;50m"); // word-level emphasis
|
|
382
|
+
let BG_DEL_W = envBg("DIFF_BG_DEL_HL", "\x1b[48;2;80;35;35m");
|
|
383
|
+
let BG_GUTTER_ADD = envBg("DIFF_BG_GUTTER_ADD", "\x1b[48;2;18;32;26m");
|
|
384
|
+
let BG_GUTTER_DEL = envBg("DIFF_BG_GUTTER_DEL", "\x1b[48;2;38;22;22m");
|
|
385
|
+
const BG_GUTTER_CTX = ""; // use terminal default bg for context gutters
|
|
386
|
+
let BG_EMPTY = "\x1b[48;2;18;18;18m"; // filler rows when one side is shorter
|
|
106
387
|
|
|
107
388
|
// Diff foregrounds — override via env: DIFF_FG_ADD="#50d264" etc.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
389
|
+
let FG_ADD = envFg("DIFF_FG_ADD", "\x1b[38;2;100;180;120m"); // desaturated green
|
|
390
|
+
let FG_DEL = envFg("DIFF_FG_DEL", "\x1b[38;2;200;100;100m"); // desaturated red
|
|
391
|
+
let FG_DIM = "\x1b[38;2;80;80;80m";
|
|
392
|
+
let FG_LNUM = "\x1b[38;2;100;100;100m";
|
|
393
|
+
let FG_RULE = "\x1b[38;2;50;50;50m";
|
|
394
|
+
let FG_SAFE_MUTED = "\x1b[38;2;139;148;158m";
|
|
114
395
|
|
|
115
|
-
|
|
396
|
+
let FG_STRIPE = "\x1b[38;2;40;40;40m"; // gray diagonal stripes on terminal default bg
|
|
116
397
|
|
|
117
398
|
const BORDER_BAR = "▌";
|
|
118
399
|
|
|
@@ -122,9 +403,13 @@ function stripes(w: number, _rowOffset: number): string {
|
|
|
122
403
|
return FG_STRIPE + "╱".repeat(w) + RST;
|
|
123
404
|
}
|
|
124
405
|
|
|
125
|
-
|
|
126
|
-
const
|
|
406
|
+
let DIVIDER = `${FG_RULE}│${RST}`;
|
|
407
|
+
const ESC_RE = "\u001b";
|
|
408
|
+
const ANSI_RE = new RegExp(`${ESC_RE}\\[[0-9;]*m`, "g");
|
|
409
|
+
const ANSI_CAPTURE_RE = new RegExp(`${ESC_RE}\\[([^m]*)m`, "g");
|
|
410
|
+
const ANSI_PARAM_CAPTURE_RE = new RegExp(`${ESC_RE}\\[([0-9;]*)m`, "g");
|
|
127
411
|
const BG_DEFAULT = "\x1b[49m"; // reset to terminal default background
|
|
412
|
+
let BG_BASE = BG_DEFAULT; // tool box base bg — updated from theme's toolSuccessBg
|
|
128
413
|
|
|
129
414
|
// ---------------------------------------------------------------------------
|
|
130
415
|
// Theme-aware diff colors
|
|
@@ -137,10 +422,27 @@ interface DiffColors {
|
|
|
137
422
|
fgCtx: string;
|
|
138
423
|
}
|
|
139
424
|
|
|
140
|
-
|
|
425
|
+
let DEFAULT_DIFF_COLORS: DiffColors = { fgAdd: FG_ADD, fgDel: FG_DEL, fgCtx: FG_DIM };
|
|
141
426
|
|
|
142
|
-
/** Resolve diff fg colors from theme (if available), falling back to hardcoded ANSI.
|
|
427
|
+
/** Resolve diff fg colors from theme (if available), falling back to hardcoded ANSI.
|
|
428
|
+
* On first call with a valid theme, auto-derives bg colors if no explicit config was set.
|
|
429
|
+
* Always reads toolSuccessBg for BG_BASE (used for context line backgrounds). */
|
|
143
430
|
function resolveDiffColors(theme?: any): DiffColors {
|
|
431
|
+
// Always read toolSuccessBg for BG_BASE (even with explicit config)
|
|
432
|
+
if (theme?.getBgAnsi && BG_BASE === BG_DEFAULT) {
|
|
433
|
+
try {
|
|
434
|
+
const bgAnsi = theme.getBgAnsi("toolSuccessBg");
|
|
435
|
+
const parsed = parseAnsiRgb(bgAnsi);
|
|
436
|
+
if (parsed) BG_BASE = bgAnsi;
|
|
437
|
+
} catch { /* ignore */ }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Auto-derive bg colors from theme on first render (if no explicit preset/overrides)
|
|
441
|
+
if (_autoDerivePending && theme?.getFgAnsi) {
|
|
442
|
+
autoDeriveBgFromTheme(theme);
|
|
443
|
+
_autoDerivePending = false;
|
|
444
|
+
}
|
|
445
|
+
|
|
144
446
|
if (!theme?.getFgAnsi) return DEFAULT_DIFF_COLORS;
|
|
145
447
|
try {
|
|
146
448
|
const fgAdd = theme.getFgAnsi("toolDiffAdded") || FG_ADD;
|
|
@@ -186,16 +488,21 @@ interface ParsedDiff {
|
|
|
186
488
|
// Utilities
|
|
187
489
|
// ---------------------------------------------------------------------------
|
|
188
490
|
|
|
189
|
-
function strip(s: string): string {
|
|
491
|
+
function strip(s: string): string {
|
|
492
|
+
return s.replace(ANSI_RE, "");
|
|
493
|
+
}
|
|
190
494
|
|
|
191
|
-
function tabs(s: string): string {
|
|
495
|
+
function tabs(s: string): string {
|
|
496
|
+
return s.replace(/\t/g, " ");
|
|
497
|
+
}
|
|
192
498
|
|
|
193
499
|
function termW(): number {
|
|
194
500
|
// Try multiple sources — process.stdout.columns may be undefined in piped/subagent contexts
|
|
195
|
-
const raw =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
||
|
|
501
|
+
const raw =
|
|
502
|
+
process.stdout.columns ||
|
|
503
|
+
(process.stderr as any).columns ||
|
|
504
|
+
Number.parseInt(process.env.COLUMNS ?? "", 10) ||
|
|
505
|
+
DEFAULT_TERM_WIDTH;
|
|
199
506
|
return Math.max(80, Math.min(raw - 4, MAX_TERM_WIDTH)); // -4 safety margin for pi TUI padding
|
|
200
507
|
}
|
|
201
508
|
|
|
@@ -206,27 +513,39 @@ function fit(s: string, w: number): string {
|
|
|
206
513
|
if (plain.length <= w) return s + " ".repeat(w - plain.length);
|
|
207
514
|
// Truncated — show content + dim › indicator
|
|
208
515
|
const showW = w > 2 ? w - 1 : w;
|
|
209
|
-
let vis = 0,
|
|
516
|
+
let vis = 0,
|
|
517
|
+
i = 0;
|
|
210
518
|
while (i < s.length && vis < showW) {
|
|
211
|
-
if (s[i] === "\x1b") {
|
|
212
|
-
|
|
519
|
+
if (s[i] === "\x1b") {
|
|
520
|
+
const e = s.indexOf("m", i);
|
|
521
|
+
if (e !== -1) {
|
|
522
|
+
i = e + 1;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
vis++;
|
|
527
|
+
i++;
|
|
213
528
|
}
|
|
214
|
-
return w > 2
|
|
215
|
-
? s.slice(0, i) + RST + FG_DIM + "›" + RST
|
|
216
|
-
: s.slice(0, i) + RST;
|
|
529
|
+
return w > 2 ? `${s.slice(0, i)}${RST}${FG_DIM}›${RST}` : `${s.slice(0, i)}${RST}`;
|
|
217
530
|
}
|
|
218
531
|
|
|
219
532
|
/** Extract last active fg + bg ANSI codes from a string. Used for wrapping continuations. */
|
|
220
533
|
function ansiState(s: string): string {
|
|
221
|
-
let fg = "",
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
if (p === "0") {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
else if (p
|
|
534
|
+
let fg = "",
|
|
535
|
+
bg = "";
|
|
536
|
+
for (const match of s.matchAll(ANSI_CAPTURE_RE)) {
|
|
537
|
+
const p = match[1] ?? "";
|
|
538
|
+
const seq = match[0] ?? "";
|
|
539
|
+
if (p === "0") {
|
|
540
|
+
fg = "";
|
|
541
|
+
bg = "";
|
|
542
|
+
} else if (p === "39") {
|
|
543
|
+
fg = "";
|
|
544
|
+
} else if (p.startsWith("38;")) {
|
|
545
|
+
fg = seq;
|
|
546
|
+
} else if (p.startsWith("48;")) {
|
|
547
|
+
bg = seq;
|
|
548
|
+
}
|
|
230
549
|
}
|
|
231
550
|
return bg + fg;
|
|
232
551
|
}
|
|
@@ -238,14 +557,14 @@ function isLowContrastShikiFg(params: string): boolean {
|
|
|
238
557
|
const parts = params.split(";").map(Number);
|
|
239
558
|
if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n))) return false;
|
|
240
559
|
const [, , r, g, b] = parts;
|
|
241
|
-
const luminance =
|
|
560
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
242
561
|
return luminance < 72;
|
|
243
562
|
}
|
|
244
563
|
|
|
245
564
|
function normalizeShikiContrast(ansi: string): string {
|
|
246
|
-
return ansi.replace(
|
|
247
|
-
isLowContrastShikiFg(params) ? FG_SAFE_MUTED : seq
|
|
248
|
-
)
|
|
565
|
+
return ansi.replace(ANSI_PARAM_CAPTURE_RE, (seq, params: string) =>
|
|
566
|
+
isLowContrastShikiFg(params) ? FG_SAFE_MUTED : seq,
|
|
567
|
+
);
|
|
249
568
|
}
|
|
250
569
|
|
|
251
570
|
/** Wrap ANSI-encoded string into rows of `w` visible chars. Max `maxRows` rows; last row truncates with ›. */
|
|
@@ -258,7 +577,9 @@ function wrapAnsi(s: string, w: number, maxRows = adaptiveWrapRows(), fillBg = "
|
|
|
258
577
|
}
|
|
259
578
|
|
|
260
579
|
const rows: string[] = [];
|
|
261
|
-
let row = "",
|
|
580
|
+
let row = "",
|
|
581
|
+
vis = 0,
|
|
582
|
+
i = 0;
|
|
262
583
|
let onLastRow = false;
|
|
263
584
|
let effW = w;
|
|
264
585
|
|
|
@@ -272,7 +593,11 @@ function wrapAnsi(s: string, w: number, maxRows = adaptiveWrapRows(), fillBg = "
|
|
|
272
593
|
// Pass through ANSI escapes
|
|
273
594
|
if (s[i] === "\x1b") {
|
|
274
595
|
const end = s.indexOf("m", i);
|
|
275
|
-
if (end !== -1) {
|
|
596
|
+
if (end !== -1) {
|
|
597
|
+
row += s.slice(i, end + 1);
|
|
598
|
+
i = end + 1;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
276
601
|
}
|
|
277
602
|
|
|
278
603
|
// Row full
|
|
@@ -281,10 +606,17 @@ function wrapAnsi(s: string, w: number, maxRows = adaptiveWrapRows(), fillBg = "
|
|
|
281
606
|
// Check if remaining string has visible chars
|
|
282
607
|
let hasMore = false;
|
|
283
608
|
for (let j = i; j < s.length; j++) {
|
|
284
|
-
if (s[j] === "\x1b") {
|
|
285
|
-
|
|
609
|
+
if (s[j] === "\x1b") {
|
|
610
|
+
const e2 = s.indexOf("m", j);
|
|
611
|
+
if (e2 !== -1) {
|
|
612
|
+
j = e2;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
hasMore = true;
|
|
617
|
+
break;
|
|
286
618
|
}
|
|
287
|
-
if (hasMore && w > 2) row += RST
|
|
619
|
+
if (hasMore && w > 2) row += `${RST}${FG_DIM}›${RST}`;
|
|
288
620
|
else row += fillBg + " ".repeat(Math.max(0, w - vis)) + RST;
|
|
289
621
|
rows.push(row);
|
|
290
622
|
return rows;
|
|
@@ -294,10 +626,15 @@ function wrapAnsi(s: string, w: number, maxRows = adaptiveWrapRows(), fillBg = "
|
|
|
294
626
|
rows.push(row + RST);
|
|
295
627
|
row = state + fillBg;
|
|
296
628
|
vis = 0;
|
|
297
|
-
if (rows.length >= maxRows - 1) {
|
|
629
|
+
if (rows.length >= maxRows - 1) {
|
|
630
|
+
onLastRow = true;
|
|
631
|
+
effW = w > 2 ? w - 1 : w;
|
|
632
|
+
}
|
|
298
633
|
}
|
|
299
634
|
|
|
300
|
-
row += s[i];
|
|
635
|
+
row += s[i];
|
|
636
|
+
vis++;
|
|
637
|
+
i++;
|
|
301
638
|
}
|
|
302
639
|
|
|
303
640
|
// Final row, padded
|
|
@@ -341,7 +678,7 @@ function shouldUseSplit(diff: ParsedDiff, tw: number, maxRows = MAX_PREVIEW_LINE
|
|
|
341
678
|
if (!diff.lines.length) return false;
|
|
342
679
|
if (tw < SPLIT_MIN_WIDTH) return false;
|
|
343
680
|
|
|
344
|
-
const nw = Math.max(2, String(Math.max(...diff.lines.map(l => l.oldNum ?? l.newNum ?? 0), 0)).length);
|
|
681
|
+
const nw = Math.max(2, String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
|
|
345
682
|
const half = Math.floor((tw - 1) / 2); // -1 for center divider
|
|
346
683
|
const gw = nw + 5; // border + num + sign + sp + │ + sp
|
|
347
684
|
const cw = Math.max(12, half - gw);
|
|
@@ -369,16 +706,43 @@ function shouldUseSplit(diff: ParsedDiff, tw: number, maxRows = MAX_PREVIEW_LINE
|
|
|
369
706
|
// ---------------------------------------------------------------------------
|
|
370
707
|
|
|
371
708
|
const EXT_LANG: Record<string, BundledLanguage> = {
|
|
372
|
-
ts: "typescript",
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
709
|
+
ts: "typescript",
|
|
710
|
+
tsx: "tsx",
|
|
711
|
+
js: "javascript",
|
|
712
|
+
jsx: "jsx",
|
|
713
|
+
mjs: "javascript",
|
|
714
|
+
cjs: "javascript",
|
|
715
|
+
py: "python",
|
|
716
|
+
rb: "ruby",
|
|
717
|
+
rs: "rust",
|
|
718
|
+
go: "go",
|
|
719
|
+
java: "java",
|
|
720
|
+
c: "c",
|
|
721
|
+
cpp: "cpp",
|
|
722
|
+
h: "c",
|
|
723
|
+
hpp: "cpp",
|
|
724
|
+
cs: "csharp",
|
|
725
|
+
swift: "swift",
|
|
726
|
+
kt: "kotlin",
|
|
727
|
+
html: "html",
|
|
728
|
+
css: "css",
|
|
729
|
+
scss: "scss",
|
|
730
|
+
json: "json",
|
|
731
|
+
yaml: "yaml",
|
|
732
|
+
yml: "yaml",
|
|
733
|
+
toml: "toml",
|
|
734
|
+
md: "markdown",
|
|
735
|
+
sql: "sql",
|
|
736
|
+
sh: "bash",
|
|
737
|
+
bash: "bash",
|
|
738
|
+
zsh: "bash",
|
|
739
|
+
lua: "lua",
|
|
740
|
+
php: "php",
|
|
741
|
+
dart: "dart",
|
|
742
|
+
xml: "xml",
|
|
743
|
+
graphql: "graphql",
|
|
744
|
+
svelte: "svelte",
|
|
745
|
+
vue: "vue",
|
|
382
746
|
};
|
|
383
747
|
|
|
384
748
|
function lang(fp: string): BundledLanguage | undefined {
|
|
@@ -396,7 +760,8 @@ codeToANSI("", "typescript", THEME).catch(() => {});
|
|
|
396
760
|
const _cache = new Map<string, string[]>();
|
|
397
761
|
|
|
398
762
|
function _touch(k: string, v: string[]): string[] {
|
|
399
|
-
_cache.delete(k);
|
|
763
|
+
_cache.delete(k);
|
|
764
|
+
_cache.set(k, v);
|
|
400
765
|
while (_cache.size > CACHE_LIMIT) {
|
|
401
766
|
const first = _cache.keys().next().value;
|
|
402
767
|
if (first === undefined) break;
|
|
@@ -429,7 +794,8 @@ async function hlBlock(code: string, language: BundledLanguage | undefined): Pro
|
|
|
429
794
|
function parseDiff(oldContent: string, newContent: string, ctx = 3): ParsedDiff {
|
|
430
795
|
const patch = Diff.structuredPatch("", "", oldContent, newContent, "", "", { context: ctx });
|
|
431
796
|
const lines: DiffLine[] = [];
|
|
432
|
-
let added = 0,
|
|
797
|
+
let added = 0,
|
|
798
|
+
removed = 0;
|
|
433
799
|
|
|
434
800
|
for (let hi = 0; hi < patch.hunks.length; hi++) {
|
|
435
801
|
if (hi > 0) {
|
|
@@ -438,13 +804,21 @@ function parseDiff(oldContent: string, newContent: string, ctx = 3): ParsedDiff
|
|
|
438
804
|
lines.push({ type: "sep", oldNum: null, newNum: gap > 0 ? gap : null, content: "" });
|
|
439
805
|
}
|
|
440
806
|
const h = patch.hunks[hi];
|
|
441
|
-
let oL = h.oldStart,
|
|
807
|
+
let oL = h.oldStart,
|
|
808
|
+
nL = h.newStart;
|
|
442
809
|
for (const raw of h.lines) {
|
|
443
810
|
if (raw === "\") continue;
|
|
444
|
-
const ch = raw[0],
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
811
|
+
const ch = raw[0],
|
|
812
|
+
text = raw.slice(1);
|
|
813
|
+
if (ch === "+") {
|
|
814
|
+
lines.push({ type: "add", oldNum: null, newNum: nL++, content: text });
|
|
815
|
+
added++;
|
|
816
|
+
} else if (ch === "-") {
|
|
817
|
+
lines.push({ type: "del", oldNum: oL++, newNum: null, content: text });
|
|
818
|
+
removed++;
|
|
819
|
+
} else {
|
|
820
|
+
lines.push({ type: "ctx", oldNum: oL++, newNum: nL++, content: text });
|
|
821
|
+
}
|
|
448
822
|
}
|
|
449
823
|
}
|
|
450
824
|
return { lines, added, removed, chars: oldContent.length + newContent.length };
|
|
@@ -464,7 +838,10 @@ function parseDiff(oldContent: string, newContent: string, ctx = 3): ParsedDiff
|
|
|
464
838
|
* similarity score and character ranges for emphasis highlighting.
|
|
465
839
|
* Replaces separate wordDiffRanges + wordDiffSimilarity (which called diffWords twice).
|
|
466
840
|
*/
|
|
467
|
-
function wordDiffAnalysis(
|
|
841
|
+
function wordDiffAnalysis(
|
|
842
|
+
a: string,
|
|
843
|
+
b: string,
|
|
844
|
+
): {
|
|
468
845
|
similarity: number;
|
|
469
846
|
oldRanges: Array<[number, number]>;
|
|
470
847
|
newRanges: Array<[number, number]>;
|
|
@@ -473,11 +850,22 @@ function wordDiffAnalysis(a: string, b: string): {
|
|
|
473
850
|
const parts = Diff.diffWords(a, b);
|
|
474
851
|
const oldRanges: Array<[number, number]> = [];
|
|
475
852
|
const newRanges: Array<[number, number]> = [];
|
|
476
|
-
let oPos = 0,
|
|
853
|
+
let oPos = 0,
|
|
854
|
+
nPos = 0,
|
|
855
|
+
same = 0;
|
|
477
856
|
for (const p of parts) {
|
|
478
|
-
if (p.removed) {
|
|
479
|
-
|
|
480
|
-
|
|
857
|
+
if (p.removed) {
|
|
858
|
+
oldRanges.push([oPos, oPos + p.value.length]);
|
|
859
|
+
oPos += p.value.length;
|
|
860
|
+
} else if (p.added) {
|
|
861
|
+
newRanges.push([nPos, nPos + p.value.length]);
|
|
862
|
+
nPos += p.value.length;
|
|
863
|
+
} else {
|
|
864
|
+
const len = p.value.length;
|
|
865
|
+
same += len;
|
|
866
|
+
oPos += len;
|
|
867
|
+
nPos += len;
|
|
868
|
+
}
|
|
481
869
|
}
|
|
482
870
|
const maxLen = Math.max(a.length, b.length);
|
|
483
871
|
return { similarity: maxLen > 0 ? same / maxLen : 1, oldRanges, newRanges };
|
|
@@ -490,12 +878,7 @@ function wordDiffAnalysis(a: string, b: string): {
|
|
|
490
878
|
*
|
|
491
879
|
* Uses sorted-range pointer scan instead of Set (avoids O(totalChars) Set creation).
|
|
492
880
|
*/
|
|
493
|
-
function injectBg(
|
|
494
|
-
ansiLine: string,
|
|
495
|
-
ranges: Array<[number, number]>,
|
|
496
|
-
baseBg: string,
|
|
497
|
-
hlBg: string,
|
|
498
|
-
): string {
|
|
881
|
+
function injectBg(ansiLine: string, ranges: Array<[number, number]>, baseBg: string, hlBg: string): string {
|
|
499
882
|
if (!ranges.length) return baseBg + ansiLine + RST;
|
|
500
883
|
|
|
501
884
|
let out = baseBg;
|
|
@@ -519,9 +902,13 @@ function injectBg(
|
|
|
519
902
|
// Advance past exhausted ranges
|
|
520
903
|
while (ri < ranges.length && vis >= ranges[ri][1]) ri++;
|
|
521
904
|
const want = ri < ranges.length && vis >= ranges[ri][0] && vis < ranges[ri][1];
|
|
522
|
-
if (want !== inHL) {
|
|
905
|
+
if (want !== inHL) {
|
|
906
|
+
inHL = want;
|
|
907
|
+
out += inHL ? hlBg : baseBg;
|
|
908
|
+
}
|
|
523
909
|
out += ansiLine[i];
|
|
524
|
-
vis++;
|
|
910
|
+
vis++;
|
|
911
|
+
i++;
|
|
525
912
|
}
|
|
526
913
|
return out + RST;
|
|
527
914
|
}
|
|
@@ -529,11 +916,15 @@ function injectBg(
|
|
|
529
916
|
/** Simple word diff (no syntax hl) — fallback when Shiki isn't available. */
|
|
530
917
|
function plainWordDiff(oldText: string, newText: string): { old: string; new: string } {
|
|
531
918
|
const parts = Diff.diffWords(oldText, newText);
|
|
532
|
-
let o = "",
|
|
919
|
+
let o = "",
|
|
920
|
+
n = "";
|
|
533
921
|
for (const p of parts) {
|
|
534
922
|
if (p.removed) o += `${BG_DEL_W}${p.value}${RST}${BG_DEL}`;
|
|
535
923
|
else if (p.added) n += `${BG_ADD_W}${p.value}${RST}${BG_ADD}`;
|
|
536
|
-
else {
|
|
924
|
+
else {
|
|
925
|
+
o += p.value;
|
|
926
|
+
n += p.value;
|
|
927
|
+
}
|
|
537
928
|
}
|
|
538
929
|
return { old: o, new: n };
|
|
539
930
|
}
|
|
@@ -549,18 +940,24 @@ function plainWordDiff(oldText: string, newText: string): { old: string; new: st
|
|
|
549
940
|
// • Paired del/add lines adjacent with word-level emphasis
|
|
550
941
|
// ---------------------------------------------------------------------------
|
|
551
942
|
|
|
552
|
-
async function renderUnified(
|
|
943
|
+
async function renderUnified(
|
|
944
|
+
diff: ParsedDiff,
|
|
945
|
+
language: BundledLanguage | undefined,
|
|
946
|
+
max = MAX_RENDER_LINES,
|
|
947
|
+
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
948
|
+
): Promise<string> {
|
|
553
949
|
if (!diff.lines.length) return "";
|
|
554
950
|
|
|
555
951
|
const vis = diff.lines.slice(0, max);
|
|
556
952
|
const tw = termW();
|
|
557
|
-
const nw = Math.max(2, String(Math.max(...vis.map(l => l.oldNum ?? l.newNum ?? 0), 0)).length);
|
|
953
|
+
const nw = Math.max(2, String(Math.max(...vis.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
|
|
558
954
|
const gw = nw + 5; // border + num + sign + sp + │ + sp
|
|
559
955
|
const cw = Math.max(20, tw - gw);
|
|
560
956
|
const canHL = diff.chars <= MAX_HL_CHARS && vis.length <= MAX_RENDER_LINES;
|
|
561
957
|
|
|
562
958
|
// Build separate old/new code blocks for highlighting
|
|
563
|
-
const oldSrc: string[] = [],
|
|
959
|
+
const oldSrc: string[] = [],
|
|
960
|
+
newSrc: string[] = [];
|
|
564
961
|
for (const l of vis) {
|
|
565
962
|
if (l.type === "ctx" || l.type === "del") oldSrc.push(l.content);
|
|
566
963
|
if (l.type === "ctx" || l.type === "add") newSrc.push(l.content);
|
|
@@ -569,14 +966,23 @@ async function renderUnified(diff: ParsedDiff, language: BundledLanguage | undef
|
|
|
569
966
|
? await Promise.all([hlBlock(oldSrc.join("\n"), language), hlBlock(newSrc.join("\n"), language)])
|
|
570
967
|
: [oldSrc, newSrc];
|
|
571
968
|
|
|
572
|
-
let oI = 0,
|
|
969
|
+
let oI = 0,
|
|
970
|
+
nI = 0,
|
|
971
|
+
idx = 0;
|
|
573
972
|
const out: string[] = [];
|
|
574
973
|
out.push(rule(tw));
|
|
575
974
|
|
|
576
975
|
/** Emit a single stacked row with compact gutter + left border bar. */
|
|
577
|
-
function emitRow(
|
|
976
|
+
function emitRow(
|
|
977
|
+
num: number | null,
|
|
978
|
+
sign: string,
|
|
979
|
+
gutterBg: string,
|
|
980
|
+
signFg: string,
|
|
981
|
+
body: string,
|
|
982
|
+
bodyBg = "",
|
|
983
|
+
): void {
|
|
578
984
|
const borderFg = sign === "-" ? dc.fgDel : sign === "+" ? dc.fgAdd : "";
|
|
579
|
-
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : `${
|
|
985
|
+
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : `${BG_BASE} `;
|
|
580
986
|
const numFg = borderFg || FG_LNUM;
|
|
581
987
|
const gutter = `${border}${gutterBg}${lnum(num, nw, numFg)}${signFg}${sign}${RST} ${DIVIDER} `;
|
|
582
988
|
const contGutter = `${border}${gutterBg}${" ".repeat(nw + 1)}${RST} ${DIVIDER} `;
|
|
@@ -594,28 +1000,35 @@ async function renderUnified(diff: ParsedDiff, language: BundledLanguage | undef
|
|
|
594
1000
|
const label = gap && gap > 0 ? ` ${gap} unmodified lines ` : "···";
|
|
595
1001
|
const totalW = Math.min(tw, 72);
|
|
596
1002
|
const pad = Math.max(0, totalW - label.length - 2);
|
|
597
|
-
const half1 = Math.floor(pad / 2),
|
|
1003
|
+
const half1 = Math.floor(pad / 2),
|
|
1004
|
+
half2 = pad - half1;
|
|
598
1005
|
out.push(`${FG_DIM}${"─".repeat(half1)}${label}${"─".repeat(half2)}${RST}`);
|
|
599
|
-
idx++;
|
|
1006
|
+
idx++;
|
|
1007
|
+
continue;
|
|
600
1008
|
}
|
|
601
1009
|
|
|
602
1010
|
// Context line — dimmed, single line number
|
|
603
1011
|
if (l.type === "ctx") {
|
|
604
1012
|
const hl = oldHL[oI] ?? l.content;
|
|
605
|
-
emitRow(l.newNum, " ",
|
|
606
|
-
oI++;
|
|
1013
|
+
emitRow(l.newNum, " ", BG_BASE, dc.fgCtx, `${BG_BASE}${DIM}${hl}`, BG_BASE);
|
|
1014
|
+
oI++;
|
|
1015
|
+
nI++;
|
|
1016
|
+
idx++;
|
|
1017
|
+
continue;
|
|
607
1018
|
}
|
|
608
1019
|
|
|
609
1020
|
// Collect del/add blocks
|
|
610
1021
|
const dels: Array<{ l: DiffLine; hl: string }> = [];
|
|
611
1022
|
while (idx < vis.length && vis[idx].type === "del") {
|
|
612
1023
|
dels.push({ l: vis[idx], hl: oldHL[oI] ?? vis[idx].content });
|
|
613
|
-
oI++;
|
|
1024
|
+
oI++;
|
|
1025
|
+
idx++;
|
|
614
1026
|
}
|
|
615
1027
|
const adds: Array<{ l: DiffLine; hl: string }> = [];
|
|
616
1028
|
while (idx < vis.length && vis[idx].type === "add") {
|
|
617
1029
|
adds.push({ l: vis[idx], hl: newHL[nI] ?? vis[idx].content });
|
|
618
|
-
nI++;
|
|
1030
|
+
nI++;
|
|
1031
|
+
idx++;
|
|
619
1032
|
}
|
|
620
1033
|
|
|
621
1034
|
// 1:1 paired → word diff emphasis
|
|
@@ -658,7 +1071,12 @@ async function renderUnified(diff: ParsedDiff, language: BundledLanguage | undef
|
|
|
658
1071
|
// Split view (auto-fallback to unified when narrow)
|
|
659
1072
|
// ---------------------------------------------------------------------------
|
|
660
1073
|
|
|
661
|
-
async function renderSplit(
|
|
1074
|
+
async function renderSplit(
|
|
1075
|
+
diff: ParsedDiff,
|
|
1076
|
+
language: BundledLanguage | undefined,
|
|
1077
|
+
max = MAX_PREVIEW_LINES,
|
|
1078
|
+
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
1079
|
+
): Promise<string> {
|
|
662
1080
|
const tw = termW();
|
|
663
1081
|
if (!shouldUseSplit(diff, tw, max)) return renderUnified(diff, language, max, dc);
|
|
664
1082
|
if (!diff.lines.length) return "";
|
|
@@ -669,23 +1087,35 @@ async function renderSplit(diff: ParsedDiff, language: BundledLanguage | undefin
|
|
|
669
1087
|
let i = 0;
|
|
670
1088
|
while (i < diff.lines.length) {
|
|
671
1089
|
const l = diff.lines[i];
|
|
672
|
-
if (l.type === "sep" || l.type === "ctx") {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1090
|
+
if (l.type === "sep" || l.type === "ctx") {
|
|
1091
|
+
rows.push({ left: l, right: l });
|
|
1092
|
+
i++;
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
const dels: DiffLine[] = [],
|
|
1096
|
+
adds: DiffLine[] = [];
|
|
1097
|
+
while (i < diff.lines.length && diff.lines[i].type === "del") {
|
|
1098
|
+
dels.push(diff.lines[i]);
|
|
1099
|
+
i++;
|
|
1100
|
+
}
|
|
1101
|
+
while (i < diff.lines.length && diff.lines[i].type === "add") {
|
|
1102
|
+
adds.push(diff.lines[i]);
|
|
1103
|
+
i++;
|
|
1104
|
+
}
|
|
676
1105
|
const n = Math.max(dels.length, adds.length);
|
|
677
1106
|
for (let j = 0; j < n; j++) rows.push({ left: dels[j] ?? null, right: adds[j] ?? null });
|
|
678
1107
|
}
|
|
679
1108
|
|
|
680
1109
|
const vis = rows.slice(0, max);
|
|
681
1110
|
const half = Math.floor((tw - 1) / 2); // -1 for center divider
|
|
682
|
-
const nw = Math.max(2, String(Math.max(...diff.lines.map(l => l.oldNum ?? l.newNum ?? 0), 0)).length);
|
|
1111
|
+
const nw = Math.max(2, String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
|
|
683
1112
|
const gw = nw + 5; // border + num + sign + sp + │ + sp
|
|
684
1113
|
const cw = Math.max(12, half - gw);
|
|
685
1114
|
const canHL = diff.chars <= MAX_HL_CHARS && vis.length * 2 <= MAX_RENDER_LINES * 2;
|
|
686
1115
|
|
|
687
1116
|
// Build separate code blocks per side
|
|
688
|
-
const leftSrc: string[] = [],
|
|
1117
|
+
const leftSrc: string[] = [],
|
|
1118
|
+
rightSrc: string[] = [];
|
|
689
1119
|
for (const r of vis) {
|
|
690
1120
|
if (r.left && r.left.type !== "sep") leftSrc.push(r.left.content);
|
|
691
1121
|
if (r.right && r.right.type !== "sep") rightSrc.push(r.right.content);
|
|
@@ -694,13 +1124,19 @@ async function renderSplit(diff: ParsedDiff, language: BundledLanguage | undefin
|
|
|
694
1124
|
? await Promise.all([hlBlock(leftSrc.join("\n"), language), hlBlock(rightSrc.join("\n"), language)])
|
|
695
1125
|
: [leftSrc, rightSrc];
|
|
696
1126
|
|
|
697
|
-
let lI = 0,
|
|
1127
|
+
let lI = 0,
|
|
1128
|
+
rI = 0;
|
|
698
1129
|
let stripeRow = 0; // tracks row index for diagonal stripe offset
|
|
699
1130
|
|
|
700
1131
|
// Returns { gutter, contGutter, body } for wrapping composition
|
|
701
1132
|
type HalfResult = { gutter: string; contGutter: string; bodyRows: string[] };
|
|
702
1133
|
|
|
703
|
-
function half_build(
|
|
1134
|
+
function half_build(
|
|
1135
|
+
line: DiffLine | null,
|
|
1136
|
+
hl: string,
|
|
1137
|
+
ranges: Array<[number, number]> | null,
|
|
1138
|
+
side: "left" | "right",
|
|
1139
|
+
): HalfResult {
|
|
704
1140
|
// Empty filler — diagonal stripes
|
|
705
1141
|
if (!line) {
|
|
706
1142
|
const gw2 = nw + 2; // number + sign + space before │
|
|
@@ -716,16 +1152,17 @@ async function renderSplit(diff: ParsedDiff, language: BundledLanguage | undefin
|
|
|
716
1152
|
return { gutter: g, contGutter: g, bodyRows: [`${FG_DIM}${fit(label, cw)}${RST}`] };
|
|
717
1153
|
}
|
|
718
1154
|
|
|
719
|
-
const isDel = line.type === "del",
|
|
720
|
-
|
|
721
|
-
const
|
|
1155
|
+
const isDel = line.type === "del",
|
|
1156
|
+
isAdd = line.type === "add";
|
|
1157
|
+
const gBg = isDel ? BG_GUTTER_DEL : isAdd ? BG_GUTTER_ADD : BG_BASE;
|
|
1158
|
+
const cBg = isDel ? BG_DEL : isAdd ? BG_ADD : BG_BASE;
|
|
722
1159
|
const sFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : dc.fgCtx;
|
|
723
1160
|
const sign = isDel ? "-" : isAdd ? "+" : " ";
|
|
724
|
-
const num = isDel ? line.oldNum : isAdd ? line.newNum :
|
|
1161
|
+
const num = isDel ? line.oldNum : isAdd ? line.newNum : side === "left" ? line.oldNum : line.newNum;
|
|
725
1162
|
|
|
726
1163
|
// Border bar + colored line numbers for changed lines
|
|
727
1164
|
const borderFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : "";
|
|
728
|
-
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : ` ${
|
|
1165
|
+
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : ` ${BG_BASE}`;
|
|
729
1166
|
const numFg = borderFg || FG_LNUM;
|
|
730
1167
|
|
|
731
1168
|
let body: string;
|
|
@@ -734,7 +1171,7 @@ async function renderSplit(diff: ParsedDiff, language: BundledLanguage | undefin
|
|
|
734
1171
|
} else if (isDel || isAdd) {
|
|
735
1172
|
body = `${cBg}${hl}`;
|
|
736
1173
|
} else {
|
|
737
|
-
body = `${
|
|
1174
|
+
body = `${BG_BASE}${DIM}${hl}`;
|
|
738
1175
|
}
|
|
739
1176
|
|
|
740
1177
|
const gutter = `${border}${gBg}${lnum(num, nw, numFg)}${sFg}${BOLD}${sign}${RST} ${FG_RULE}│${RST} `;
|
|
@@ -751,7 +1188,8 @@ async function renderSplit(diff: ParsedDiff, language: BundledLanguage | undefin
|
|
|
751
1188
|
out.push(`${rule(half)}${FG_RULE}┊${RST}${rule(half)}`);
|
|
752
1189
|
|
|
753
1190
|
for (const r of vis) {
|
|
754
|
-
const leftLine = r.left,
|
|
1191
|
+
const leftLine = r.left,
|
|
1192
|
+
rightLine = r.right;
|
|
755
1193
|
const paired = leftLine && rightLine && leftLine.type === "del" && rightLine.type === "add";
|
|
756
1194
|
const wd = paired ? wordDiffAnalysis(leftLine.content, rightLine.content) : null;
|
|
757
1195
|
|
|
@@ -764,12 +1202,13 @@ async function renderSplit(diff: ParsedDiff, language: BundledLanguage | undefin
|
|
|
764
1202
|
rResult = half_build(rightLine, rhl, wd.newRanges, "right");
|
|
765
1203
|
} else if (paired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
|
|
766
1204
|
const pwd = plainWordDiff(leftLine.content, rightLine.content);
|
|
767
|
-
lI++;
|
|
1205
|
+
lI++;
|
|
1206
|
+
rI++;
|
|
768
1207
|
lResult = half_build(leftLine, pwd.old, null, "left");
|
|
769
1208
|
rResult = half_build(rightLine, pwd.new, null, "right");
|
|
770
1209
|
} else {
|
|
771
|
-
const lhl =
|
|
772
|
-
const rhl =
|
|
1210
|
+
const lhl = leftLine && leftLine.type !== "sep" ? (leftHL[lI++] ?? leftLine?.content ?? "") : "";
|
|
1211
|
+
const rhl = rightLine && rightLine.type !== "sep" ? (rightHL[rI++] ?? rightLine?.content ?? "") : "";
|
|
773
1212
|
lResult = half_build(leftLine, lhl, null, "left");
|
|
774
1213
|
rResult = half_build(rightLine, rhl, null, "right");
|
|
775
1214
|
}
|
|
@@ -782,7 +1221,8 @@ async function renderSplit(diff: ParsedDiff, language: BundledLanguage | undefin
|
|
|
782
1221
|
const lg = row === 0 ? lResult.gutter : lResult.contGutter;
|
|
783
1222
|
const rg = row === 0 ? rResult.gutter : rResult.contGutter;
|
|
784
1223
|
const lb = lResult.bodyRows[row] ?? (leftIsEmpty ? stripes(cw, stripeRow) : `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
|
|
785
|
-
const rb =
|
|
1224
|
+
const rb =
|
|
1225
|
+
rResult.bodyRows[row] ?? (rightIsEmpty ? stripes(cw, stripeRow) : `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
|
|
786
1226
|
out.push(`${lg}${lb}${DIVIDER}${rg}${rb}`);
|
|
787
1227
|
stripeRow++;
|
|
788
1228
|
}
|
|
@@ -807,13 +1247,18 @@ export const __testing = {
|
|
|
807
1247
|
};
|
|
808
1248
|
|
|
809
1249
|
export default function diffRendererExtension(pi: any): void {
|
|
1250
|
+
// Apply diff theme palette from settings/presets before rendering
|
|
1251
|
+
applyDiffPalette();
|
|
1252
|
+
|
|
810
1253
|
let createWriteTool: any, createEditTool: any, TextComponent: any;
|
|
811
1254
|
try {
|
|
812
1255
|
const sdk = require("@mariozechner/pi-coding-agent");
|
|
813
1256
|
createWriteTool = sdk.createWriteTool;
|
|
814
1257
|
createEditTool = sdk.createEditTool;
|
|
815
1258
|
TextComponent = require("@mariozechner/pi-tui").Text;
|
|
816
|
-
} catch {
|
|
1259
|
+
} catch {
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
817
1262
|
if (!createWriteTool || !createEditTool || !TextComponent) return;
|
|
818
1263
|
|
|
819
1264
|
const cwd = process.cwd();
|
|
@@ -833,7 +1278,11 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
833
1278
|
async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
|
|
834
1279
|
const fp = params.path ?? params.file_path ?? "";
|
|
835
1280
|
let old: string | null = null;
|
|
836
|
-
try {
|
|
1281
|
+
try {
|
|
1282
|
+
if (fp && existsSync(fp)) old = readFileSync(fp, "utf-8");
|
|
1283
|
+
} catch {
|
|
1284
|
+
old = null;
|
|
1285
|
+
}
|
|
837
1286
|
|
|
838
1287
|
const result = await origWrite.execute(tid, params, sig, upd, ctx);
|
|
839
1288
|
const content = params.content ?? "";
|
|
@@ -873,16 +1322,18 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
873
1322
|
ctx.state._previewKey = previewKey;
|
|
874
1323
|
ctx.state._previewText = hdr;
|
|
875
1324
|
const lg = lang(fp);
|
|
876
|
-
hlBlock(args.content, lg)
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1325
|
+
hlBlock(args.content, lg)
|
|
1326
|
+
.then((lines: string[]) => {
|
|
1327
|
+
if (ctx.state._previewKey !== previewKey) return;
|
|
1328
|
+
const maxShow = ctx.expanded ? lines.length : 16;
|
|
1329
|
+
const preview = lines.slice(0, maxShow).join("\n");
|
|
1330
|
+
const rem = lines.length - maxShow;
|
|
1331
|
+
let out = `${hdr}\n\n${preview}`;
|
|
1332
|
+
if (rem > 0) out += `\n${theme.fg("muted", `… (${rem} more lines, ${lines.length} total)`)}`;
|
|
1333
|
+
ctx.state._previewText = out;
|
|
1334
|
+
ctx.invalidate();
|
|
1335
|
+
})
|
|
1336
|
+
.catch(() => {});
|
|
886
1337
|
}
|
|
887
1338
|
text.setText(ctx.state._previewText ?? hdr);
|
|
888
1339
|
return text;
|
|
@@ -895,7 +1346,11 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
895
1346
|
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
896
1347
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
897
1348
|
if (ctx.isError) {
|
|
898
|
-
const e =
|
|
1349
|
+
const e =
|
|
1350
|
+
result.content
|
|
1351
|
+
?.filter((c: any) => c.type === "text")
|
|
1352
|
+
.map((c: any) => c.text || "")
|
|
1353
|
+
.join("\n") ?? "Error";
|
|
899
1354
|
text.setText(`\n${theme.fg("error", e)}`);
|
|
900
1355
|
return text;
|
|
901
1356
|
}
|
|
@@ -907,15 +1362,17 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
907
1362
|
ctx.state._wdk = key;
|
|
908
1363
|
ctx.state._wdt = ` ${d.summary}\n${theme.fg("muted", " rendering diff…")}`;
|
|
909
1364
|
const dc = resolveDiffColors(theme);
|
|
910
|
-
renderSplit(d.diff, d.language, MAX_RENDER_LINES, dc)
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1365
|
+
renderSplit(d.diff, d.language, MAX_RENDER_LINES, dc)
|
|
1366
|
+
.then((rendered: string) => {
|
|
1367
|
+
if (ctx.state._wdk !== key) return;
|
|
1368
|
+
ctx.state._wdt = ` ${d.summary}\n${rendered}`;
|
|
1369
|
+
ctx.invalidate();
|
|
1370
|
+
})
|
|
1371
|
+
.catch(() => {
|
|
1372
|
+
if (ctx.state._wdk !== key) return;
|
|
1373
|
+
ctx.state._wdt = ` ${d.summary}`;
|
|
1374
|
+
ctx.invalidate();
|
|
1375
|
+
});
|
|
919
1376
|
}
|
|
920
1377
|
text.setText(ctx.state._wdt ?? ` ${d.summary}`);
|
|
921
1378
|
return text;
|
|
@@ -932,16 +1389,18 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
932
1389
|
ctx.state._nft = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`;
|
|
933
1390
|
const lg = lang(fp);
|
|
934
1391
|
if (rawContent) {
|
|
935
|
-
hlBlock(rawContent, lg)
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1392
|
+
hlBlock(rawContent, lg)
|
|
1393
|
+
.then((hlLines: string[]) => {
|
|
1394
|
+
if (ctx.state._nfk !== pk) return;
|
|
1395
|
+
const maxShow = ctx.expanded ? hlLines.length : 12;
|
|
1396
|
+
const preview = hlLines.slice(0, maxShow).join("\n");
|
|
1397
|
+
const rem = hlLines.length - maxShow;
|
|
1398
|
+
let out = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}\n${preview}`;
|
|
1399
|
+
if (rem > 0) out += `\n${theme.fg("muted", ` … ${rem} more lines`)}`;
|
|
1400
|
+
ctx.state._nft = out;
|
|
1401
|
+
ctx.invalidate();
|
|
1402
|
+
})
|
|
1403
|
+
.catch(() => {});
|
|
945
1404
|
}
|
|
946
1405
|
}
|
|
947
1406
|
text.setText(ctx.state._nft ?? ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`);
|
|
@@ -977,7 +1436,9 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
977
1436
|
const idx = f.indexOf(newText);
|
|
978
1437
|
if (idx >= 0) editLine = f.slice(0, idx).split("\n").length;
|
|
979
1438
|
}
|
|
980
|
-
} catch {
|
|
1439
|
+
} catch {
|
|
1440
|
+
editLine = 0;
|
|
1441
|
+
}
|
|
981
1442
|
const diff = parseDiff(oldText, newText);
|
|
982
1443
|
(result as any).details = { _type: "editInfo", summary: summarize(diff.added, diff.removed), editLine };
|
|
983
1444
|
}
|
|
@@ -1003,17 +1464,19 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
1003
1464
|
const lg = lang(fp);
|
|
1004
1465
|
const diff = parseDiff(oldText, newText);
|
|
1005
1466
|
const dc = resolveDiffColors(theme);
|
|
1006
|
-
renderSplit(diff, lg, MAX_PREVIEW_LINES, dc)
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1467
|
+
renderSplit(diff, lg, MAX_PREVIEW_LINES, dc)
|
|
1468
|
+
.then((rendered) => {
|
|
1469
|
+
if (ctx.state._pk !== pk) return;
|
|
1470
|
+
ctx.state._pt = `${hdr}\n${summarize(diff.added, diff.removed)}\n${rendered}`;
|
|
1471
|
+
ctx.invalidate();
|
|
1472
|
+
})
|
|
1473
|
+
.catch(() => {
|
|
1474
|
+
if (ctx.state._pk !== pk) return;
|
|
1475
|
+
// Fallback: plain word diff
|
|
1476
|
+
const diff2 = parseDiff(oldText, newText);
|
|
1477
|
+
ctx.state._pt = `${hdr} ${summarize(diff2.added, diff2.removed)}`;
|
|
1478
|
+
ctx.invalidate();
|
|
1479
|
+
});
|
|
1017
1480
|
}
|
|
1018
1481
|
|
|
1019
1482
|
text.setText(ctx.state._pt ?? hdr);
|
|
@@ -1023,7 +1486,11 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
1023
1486
|
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
1024
1487
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1025
1488
|
if (ctx.isError) {
|
|
1026
|
-
const e =
|
|
1489
|
+
const e =
|
|
1490
|
+
result.content
|
|
1491
|
+
?.filter((c: any) => c.type === "text")
|
|
1492
|
+
.map((c: any) => c.text || "")
|
|
1493
|
+
.join("\n") ?? "Error";
|
|
1027
1494
|
text.setText(`\n${theme.fg("error", e)}`);
|
|
1028
1495
|
return text;
|
|
1029
1496
|
}
|