@xynogen/pix-pretty 1.0.0
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/LICENSE +21 -0
- package/README.md +68 -0
- package/package.json +54 -0
- package/src/README.md +66 -0
- package/src/ansi.ts +89 -0
- package/src/config.ts +66 -0
- package/src/diff-render.ts +892 -0
- package/src/diff.ts +68 -0
- package/src/fff.ts +416 -0
- package/src/highlight.ts +118 -0
- package/src/icons.ts +120 -0
- package/src/image.ts +166 -0
- package/src/index.test.ts +33 -0
- package/src/index.ts +1623 -0
- package/src/lang.ts +67 -0
- package/src/paste-chips.test.ts +138 -0
- package/src/paste-chips.ts +160 -0
- package/src/renderers.ts +222 -0
- package/src/thinking.test.ts +223 -0
- package/src/thinking.ts +100 -0
- package/src/tsconfig.json +14 -0
- package/src/types-diff.d.ts +41 -0
- package/src/types-fff.d.ts +80 -0
- package/src/types.ts +275 -0
- package/src/utils.ts +180 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
// Split / unified / word-level diff rendering — ported from
|
|
2
|
+
// @heyhuynhgiabuu/pi-diff (src/index.ts render core) and adapted to the
|
|
3
|
+
// vendored pretty extension's primitives (cli-highlight `hlBlock`, shared
|
|
4
|
+
// theme-aware `RST`/`BG_BASE` from ansi.ts).
|
|
5
|
+
//
|
|
6
|
+
// Engine note: pi-diff used Shiki's codeToANSI (fg-only output). pretty's
|
|
7
|
+
// hlBlock (cli-highlight) likewise emits only fg codes, so the bg-injection
|
|
8
|
+
// technique below works unchanged — diff backgrounds layer underneath and
|
|
9
|
+
// persist through fg switches.
|
|
10
|
+
|
|
11
|
+
import * as Diff from "diff";
|
|
12
|
+
import { BG_BASE, BOLD, FG_DIM, FG_LNUM, FG_RULE, RST } from "./ansi.js";
|
|
13
|
+
import { MAX_HL_CHARS, MAX_RENDER_LINES, WORD_DIFF_MIN_SIM } from "./config.js";
|
|
14
|
+
import type { DiffLine, ParsedDiff } from "./diff.js";
|
|
15
|
+
import { hlBlock } from "./highlight.js";
|
|
16
|
+
import type { BundledLanguage } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Env-overridable color/threshold helpers (mirror pi-diff)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
function envInt(name: string, fallback: number): number {
|
|
23
|
+
const v = Number.parseInt(process.env[name] ?? "", 10);
|
|
24
|
+
return Number.isFinite(v) ? v : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function envFg(name: string, fallback: string): string {
|
|
28
|
+
const hex = process.env[name];
|
|
29
|
+
if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return fallback;
|
|
30
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
31
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
32
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
33
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function envBg(name: string, fallback: string): string {
|
|
37
|
+
const hex = process.env[name];
|
|
38
|
+
if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return fallback;
|
|
39
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
40
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
41
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
42
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Diff-specific ANSI (override via env, hex "#RRGGBB")
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const DIM = "\x1b[2m";
|
|
50
|
+
|
|
51
|
+
// Subtle diff backgrounds — muted tones to let syntax fg shine through.
|
|
52
|
+
const BG_ADD = envBg("DIFF_BG_ADD", "\x1b[48;2;22;38;32m"); // muted teal-green
|
|
53
|
+
const BG_DEL = envBg("DIFF_BG_DEL", "\x1b[48;2;45;25;25m"); // muted brown-red
|
|
54
|
+
const BG_ADD_W = envBg("DIFF_BG_ADD_HL", "\x1b[48;2;35;75;50m"); // word emphasis
|
|
55
|
+
const BG_DEL_W = envBg("DIFF_BG_DEL_HL", "\x1b[48;2;80;35;35m");
|
|
56
|
+
const BG_GUTTER_ADD = envBg("DIFF_BG_GUTTER_ADD", "\x1b[48;2;18;32;26m");
|
|
57
|
+
const BG_GUTTER_DEL = envBg("DIFF_BG_GUTTER_DEL", "\x1b[48;2;38;22;22m");
|
|
58
|
+
const BG_EMPTY = "\x1b[48;2;18;18;18m"; // filler rows when one side is shorter
|
|
59
|
+
|
|
60
|
+
const FG_ADD = envFg("DIFF_FG_ADD", "\x1b[38;2;100;180;120m"); // desaturated green
|
|
61
|
+
const FG_DEL = envFg("DIFF_FG_DEL", "\x1b[38;2;200;100;100m"); // desaturated red
|
|
62
|
+
const FG_STRIPE = "\x1b[38;2;40;40;40m"; // diagonal stripes on filler cells
|
|
63
|
+
|
|
64
|
+
const BORDER_BAR = "▌";
|
|
65
|
+
const DIVIDER = `${FG_RULE}│${RST}`;
|
|
66
|
+
|
|
67
|
+
const ESC_RE = "\u001b";
|
|
68
|
+
const ANSI_RE = new RegExp(`${ESC_RE}\\[[0-9;]*m`, "g");
|
|
69
|
+
const ANSI_CAPTURE_RE = new RegExp(`${ESC_RE}\\[([^m]*)m`, "g");
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Terminal bounds + thresholds
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const MAX_TERM_WIDTH = 210;
|
|
76
|
+
const DEFAULT_TERM_WIDTH = 200;
|
|
77
|
+
|
|
78
|
+
const MAX_PREVIEW_LINES = envInt("PRETTY_MAX_PREVIEW_LINES", 80);
|
|
79
|
+
|
|
80
|
+
const SPLIT_MIN_WIDTH = envInt("DIFF_SPLIT_MIN_WIDTH", 150);
|
|
81
|
+
const SPLIT_MIN_CODE_WIDTH = envInt("DIFF_SPLIT_MIN_CODE_WIDTH", 60);
|
|
82
|
+
const SPLIT_MAX_WRAP_RATIO = 0.2;
|
|
83
|
+
const SPLIT_MAX_WRAP_LINES = 8;
|
|
84
|
+
|
|
85
|
+
const MAX_WRAP_ROWS_WIDE = 3; // >=180 cols
|
|
86
|
+
const MAX_WRAP_ROWS_MED = 2; // 120-179 cols
|
|
87
|
+
const MAX_WRAP_ROWS_NARROW = 1; // <120 cols (truncate)
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Theme-aware diff colors
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export interface DiffColors {
|
|
94
|
+
fgAdd: string;
|
|
95
|
+
fgDel: string;
|
|
96
|
+
fgCtx: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const DEFAULT_DIFF_COLORS: DiffColors = {
|
|
100
|
+
fgAdd: FG_ADD,
|
|
101
|
+
fgDel: FG_DEL,
|
|
102
|
+
fgCtx: FG_DIM,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// --- contrast helpers -------------------------------------------------------
|
|
106
|
+
// The gutter (line number + sign) paints the diff fg over a dark gutter bg.
|
|
107
|
+
// A theme whose diff fg is itself dark renders the number/sign as black-on-
|
|
108
|
+
// black. We keep the theme's hue but lift its luminance until it clears a
|
|
109
|
+
// minimum contrast ratio against the gutter background it sits on.
|
|
110
|
+
|
|
111
|
+
type Rgb = [number, number, number];
|
|
112
|
+
|
|
113
|
+
function parseAnsiRgb(seq: string, kind: "38" | "48"): Rgb | null {
|
|
114
|
+
const m = seq.match(new RegExp(`\\x1b\\[${kind};2;(\\d+);(\\d+);(\\d+)m`));
|
|
115
|
+
if (!m) return null;
|
|
116
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function relLuminance([r, g, b]: Rgb): number {
|
|
120
|
+
const f = (c: number) => {
|
|
121
|
+
const s = c / 255;
|
|
122
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
123
|
+
};
|
|
124
|
+
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function contrastRatio(a: Rgb, b: Rgb): number {
|
|
128
|
+
const la = relLuminance(a);
|
|
129
|
+
const lb = relLuminance(b);
|
|
130
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la];
|
|
131
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Keep hue, raise lightness toward white until contrast >= min (or capped). */
|
|
135
|
+
function ensureContrast(fg: string, bgSeq: string, min = 3): string {
|
|
136
|
+
const rgb = parseAnsiRgb(fg, "38");
|
|
137
|
+
const bg = parseAnsiRgb(bgSeq, "48");
|
|
138
|
+
if (!rgb || !bg) return fg; // can't reason about it — leave theme value
|
|
139
|
+
if (contrastRatio(rgb, bg) >= min) return fg; // already legible
|
|
140
|
+
let [r, g, b] = rgb;
|
|
141
|
+
for (let i = 0; i < 12 && contrastRatio([r, g, b], bg) < min; i++) {
|
|
142
|
+
r = Math.round(r + (255 - r) * 0.25);
|
|
143
|
+
g = Math.round(g + (255 - g) * 0.25);
|
|
144
|
+
b = Math.round(b + (255 - b) * 0.25);
|
|
145
|
+
}
|
|
146
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Resolve diff fg colors from pi's theme (if it exposes getFgAnsi), falling
|
|
150
|
+
* back to hardcoded ANSI. BG_BASE is already kept in sync by ansi.ts's
|
|
151
|
+
* resolveBaseBackground (called from the tool renderers).
|
|
152
|
+
*
|
|
153
|
+
* Theme hue is preserved, but each add/del fg is contrast-checked against the
|
|
154
|
+
* gutter bg it is painted on and lifted if it would render too dark to read. */
|
|
155
|
+
export function resolveDiffColors(theme?: {
|
|
156
|
+
getFgAnsi?: (key: string) => string;
|
|
157
|
+
}): DiffColors {
|
|
158
|
+
if (!theme?.getFgAnsi) return DEFAULT_DIFF_COLORS;
|
|
159
|
+
try {
|
|
160
|
+
return {
|
|
161
|
+
fgAdd: ensureContrast(
|
|
162
|
+
theme.getFgAnsi("toolDiffAdded") || FG_ADD,
|
|
163
|
+
BG_GUTTER_ADD,
|
|
164
|
+
),
|
|
165
|
+
fgDel: ensureContrast(
|
|
166
|
+
theme.getFgAnsi("toolDiffRemoved") || FG_DEL,
|
|
167
|
+
BG_GUTTER_DEL,
|
|
168
|
+
),
|
|
169
|
+
fgCtx: theme.getFgAnsi("toolDiffContext") || FG_DIM,
|
|
170
|
+
};
|
|
171
|
+
} catch {
|
|
172
|
+
return DEFAULT_DIFF_COLORS;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Stable cache key for the resolved diff theme colors. */
|
|
177
|
+
export function diffThemeCacheKey(theme?: {
|
|
178
|
+
getFgAnsi?: (key: string) => string;
|
|
179
|
+
}): string {
|
|
180
|
+
const c = resolveDiffColors(theme);
|
|
181
|
+
return `${c.fgAdd}|${c.fgDel}|${c.fgCtx}|${BG_BASE}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Adaptive helpers + utilities
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function adaptiveWrapRows(tw?: number): number {
|
|
189
|
+
const w = tw ?? termW();
|
|
190
|
+
if (w >= 180) return MAX_WRAP_ROWS_WIDE;
|
|
191
|
+
if (w >= 120) return MAX_WRAP_ROWS_MED;
|
|
192
|
+
return MAX_WRAP_ROWS_NARROW;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function strip(s: string): string {
|
|
196
|
+
return s.replace(ANSI_RE, "");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function tabs(s: string): string {
|
|
200
|
+
return s.replace(/\t/g, " ");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function termW(): number {
|
|
204
|
+
const raw =
|
|
205
|
+
process.stdout.columns ||
|
|
206
|
+
(process.stderr as { columns?: number }).columns ||
|
|
207
|
+
Number.parseInt(process.env.COLUMNS ?? "", 10) ||
|
|
208
|
+
DEFAULT_TERM_WIDTH;
|
|
209
|
+
return Math.max(80, Math.min(raw - 4, MAX_TERM_WIDTH));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Pad/truncate `s` to exactly `w` visible chars. ANSI-aware. */
|
|
213
|
+
function fit(s: string, w: number): string {
|
|
214
|
+
if (w <= 0) return "";
|
|
215
|
+
const plain = strip(s);
|
|
216
|
+
if (plain.length <= w) return s + " ".repeat(w - plain.length);
|
|
217
|
+
const showW = w > 2 ? w - 1 : w;
|
|
218
|
+
let vis = 0;
|
|
219
|
+
let i = 0;
|
|
220
|
+
while (i < s.length && vis < showW) {
|
|
221
|
+
if (s[i] === "\x1b") {
|
|
222
|
+
const e = s.indexOf("m", i);
|
|
223
|
+
if (e !== -1) {
|
|
224
|
+
i = e + 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
vis++;
|
|
229
|
+
i++;
|
|
230
|
+
}
|
|
231
|
+
return w > 2
|
|
232
|
+
? `${s.slice(0, i)}${RST}${FG_DIM}›${RST}`
|
|
233
|
+
: `${s.slice(0, i)}${RST}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Extract last active fg + bg ANSI codes from a string (for wrap continuations). */
|
|
237
|
+
function ansiState(s: string): string {
|
|
238
|
+
let fg = "";
|
|
239
|
+
let bg = "";
|
|
240
|
+
for (const match of s.matchAll(ANSI_CAPTURE_RE)) {
|
|
241
|
+
const p = match[1] ?? "";
|
|
242
|
+
const seq = match[0] ?? "";
|
|
243
|
+
if (p === "0") {
|
|
244
|
+
fg = "";
|
|
245
|
+
bg = "";
|
|
246
|
+
} else if (p === "39") {
|
|
247
|
+
fg = "";
|
|
248
|
+
} else if (p.startsWith("38;")) {
|
|
249
|
+
fg = seq;
|
|
250
|
+
} else if (p.startsWith("48;")) {
|
|
251
|
+
bg = seq;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return bg + fg;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Wrap ANSI-encoded string into rows of `w` visible chars. */
|
|
258
|
+
function wrapAnsi(
|
|
259
|
+
s: string,
|
|
260
|
+
w: number,
|
|
261
|
+
maxRows = adaptiveWrapRows(),
|
|
262
|
+
fillBg = "",
|
|
263
|
+
): string[] {
|
|
264
|
+
if (w <= 0) return [""];
|
|
265
|
+
const plain = strip(s);
|
|
266
|
+
if (plain.length <= w) {
|
|
267
|
+
const pad = w - plain.length;
|
|
268
|
+
return pad > 0 ? [s + fillBg + " ".repeat(pad) + (fillBg ? RST : "")] : [s];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const rows: string[] = [];
|
|
272
|
+
let row = "";
|
|
273
|
+
let vis = 0;
|
|
274
|
+
let i = 0;
|
|
275
|
+
let onLastRow = false;
|
|
276
|
+
let effW = w;
|
|
277
|
+
|
|
278
|
+
while (i < s.length) {
|
|
279
|
+
if (!onLastRow && rows.length >= maxRows - 1) {
|
|
280
|
+
onLastRow = true;
|
|
281
|
+
effW = w > 2 ? w - 1 : w;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (s[i] === "\x1b") {
|
|
285
|
+
const end = s.indexOf("m", i);
|
|
286
|
+
if (end !== -1) {
|
|
287
|
+
row += s.slice(i, end + 1);
|
|
288
|
+
i = end + 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (vis >= effW) {
|
|
294
|
+
if (onLastRow) {
|
|
295
|
+
let hasMore = false;
|
|
296
|
+
for (let j = i; j < s.length; j++) {
|
|
297
|
+
if (s[j] === "\x1b") {
|
|
298
|
+
const e2 = s.indexOf("m", j);
|
|
299
|
+
if (e2 !== -1) {
|
|
300
|
+
j = e2;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
hasMore = true;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
if (hasMore && w > 2) row += `${RST}${FG_DIM}›${RST}`;
|
|
308
|
+
else row += fillBg + " ".repeat(Math.max(0, w - vis)) + RST;
|
|
309
|
+
rows.push(row);
|
|
310
|
+
return rows;
|
|
311
|
+
}
|
|
312
|
+
const state = ansiState(row);
|
|
313
|
+
rows.push(row + RST);
|
|
314
|
+
row = state + fillBg;
|
|
315
|
+
vis = 0;
|
|
316
|
+
if (rows.length >= maxRows - 1) {
|
|
317
|
+
onLastRow = true;
|
|
318
|
+
effW = w > 2 ? w - 1 : w;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
row += s[i];
|
|
323
|
+
vis++;
|
|
324
|
+
i++;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (row.length > 0 || rows.length === 0) {
|
|
328
|
+
rows.push(row + fillBg + " ".repeat(Math.max(0, w - vis)) + RST);
|
|
329
|
+
}
|
|
330
|
+
return rows;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Dense diagonal stripe fill for empty filler cells. */
|
|
334
|
+
function stripes(w: number, _rowOffset: number): string {
|
|
335
|
+
return BG_BASE + FG_STRIPE + "╱".repeat(w) + RST;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function lnum(n: number | null, w: number, fg = FG_LNUM): string {
|
|
339
|
+
if (n === null) return " ".repeat(w);
|
|
340
|
+
const v = String(n);
|
|
341
|
+
return `${fg}${" ".repeat(Math.max(0, w - v.length))}${v}${RST}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function rule(w: number): string {
|
|
345
|
+
return `${BG_BASE}${FG_RULE}${"─".repeat(w)}${RST}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Compact "+a -d" summary string (or "no changes"). */
|
|
349
|
+
export function summarize(a: number, d: number): string {
|
|
350
|
+
const p: string[] = [];
|
|
351
|
+
if (a > 0) p.push(`${FG_ADD}+${a}${RST}`);
|
|
352
|
+
if (d > 0) p.push(`${FG_DEL}-${d}${RST}`);
|
|
353
|
+
return p.length ? p.join(" ") : `${FG_DIM}no changes${RST}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Word diff + bg injection
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
function wordDiffAnalysis(
|
|
361
|
+
a: string,
|
|
362
|
+
b: string,
|
|
363
|
+
): {
|
|
364
|
+
similarity: number;
|
|
365
|
+
oldRanges: Array<[number, number]>;
|
|
366
|
+
newRanges: Array<[number, number]>;
|
|
367
|
+
} {
|
|
368
|
+
if (!a && !b) return { similarity: 1, oldRanges: [], newRanges: [] };
|
|
369
|
+
const parts = Diff.diffWords(a, b);
|
|
370
|
+
const oldRanges: Array<[number, number]> = [];
|
|
371
|
+
const newRanges: Array<[number, number]> = [];
|
|
372
|
+
let oPos = 0;
|
|
373
|
+
let nPos = 0;
|
|
374
|
+
let same = 0;
|
|
375
|
+
for (const p of parts) {
|
|
376
|
+
if (p.removed) {
|
|
377
|
+
oldRanges.push([oPos, oPos + p.value.length]);
|
|
378
|
+
oPos += p.value.length;
|
|
379
|
+
} else if (p.added) {
|
|
380
|
+
newRanges.push([nPos, nPos + p.value.length]);
|
|
381
|
+
nPos += p.value.length;
|
|
382
|
+
} else {
|
|
383
|
+
const len = p.value.length;
|
|
384
|
+
same += len;
|
|
385
|
+
oPos += len;
|
|
386
|
+
nPos += len;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const maxLen = Math.max(a.length, b.length);
|
|
390
|
+
return {
|
|
391
|
+
similarity: maxLen > 0 ? same / maxLen : 1,
|
|
392
|
+
oldRanges,
|
|
393
|
+
newRanges,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Inject diff background into fg-only highlighted output.
|
|
398
|
+
* `baseBg` on unchanged spans, `hlBg` on changed char ranges.
|
|
399
|
+
* Re-injects bg after any reset-like sequence. */
|
|
400
|
+
function injectBg(
|
|
401
|
+
ansiLine: string,
|
|
402
|
+
ranges: Array<[number, number]>,
|
|
403
|
+
baseBg: string,
|
|
404
|
+
hlBg: string,
|
|
405
|
+
): string {
|
|
406
|
+
if (!ranges.length) return baseBg + ansiLine + RST;
|
|
407
|
+
|
|
408
|
+
let out = baseBg;
|
|
409
|
+
let vis = 0;
|
|
410
|
+
let inHL = false;
|
|
411
|
+
let ri = 0;
|
|
412
|
+
let i = 0;
|
|
413
|
+
|
|
414
|
+
while (i < ansiLine.length) {
|
|
415
|
+
if (ansiLine[i] === "\x1b") {
|
|
416
|
+
const m = ansiLine.indexOf("m", i);
|
|
417
|
+
if (m !== -1) {
|
|
418
|
+
const seq = ansiLine.slice(i, m + 1);
|
|
419
|
+
out += seq;
|
|
420
|
+
if (seq === "\x1b[0m" || seq === "\x1b[39m" || seq === "\x1b[49m") {
|
|
421
|
+
out += inHL ? hlBg : baseBg;
|
|
422
|
+
}
|
|
423
|
+
i = m + 1;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
while (ri < ranges.length && vis >= ranges[ri][1]) ri++;
|
|
428
|
+
const want =
|
|
429
|
+
ri < ranges.length && vis >= ranges[ri][0] && vis < ranges[ri][1];
|
|
430
|
+
if (want !== inHL) {
|
|
431
|
+
inHL = want;
|
|
432
|
+
out += inHL ? hlBg : baseBg;
|
|
433
|
+
}
|
|
434
|
+
out += ansiLine[i];
|
|
435
|
+
vis++;
|
|
436
|
+
i++;
|
|
437
|
+
}
|
|
438
|
+
return out + RST;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Simple word diff (no syntax hl) — fallback when highlighting is unavailable. */
|
|
442
|
+
function plainWordDiff(
|
|
443
|
+
oldText: string,
|
|
444
|
+
newText: string,
|
|
445
|
+
): { old: string; new: string } {
|
|
446
|
+
const parts = Diff.diffWords(oldText, newText);
|
|
447
|
+
let o = "";
|
|
448
|
+
let n = "";
|
|
449
|
+
for (const p of parts) {
|
|
450
|
+
if (p.removed) o += `${BG_DEL_W}${p.value}${RST}${BG_DEL}`;
|
|
451
|
+
else if (p.added) n += `${BG_ADD_W}${p.value}${RST}${BG_ADD}`;
|
|
452
|
+
else {
|
|
453
|
+
o += p.value;
|
|
454
|
+
n += p.value;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return { old: o, new: n };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Split-vs-unified decision
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
function shouldUseSplit(
|
|
465
|
+
diff: ParsedDiff,
|
|
466
|
+
tw: number,
|
|
467
|
+
maxRows = MAX_PREVIEW_LINES,
|
|
468
|
+
): boolean {
|
|
469
|
+
if (!diff.lines.length) return false;
|
|
470
|
+
if (tw < SPLIT_MIN_WIDTH) return false;
|
|
471
|
+
|
|
472
|
+
const nw = Math.max(
|
|
473
|
+
2,
|
|
474
|
+
String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0))
|
|
475
|
+
.length,
|
|
476
|
+
);
|
|
477
|
+
const half = Math.floor((tw - 1) / 2);
|
|
478
|
+
const gw = nw + 5;
|
|
479
|
+
const cw = Math.max(12, half - gw);
|
|
480
|
+
if (cw < SPLIT_MIN_CODE_WIDTH) return false;
|
|
481
|
+
|
|
482
|
+
const vis = diff.lines.slice(0, maxRows);
|
|
483
|
+
let contentLines = 0;
|
|
484
|
+
let wrapCandidates = 0;
|
|
485
|
+
for (const l of vis) {
|
|
486
|
+
if (l.type === "sep") continue;
|
|
487
|
+
contentLines++;
|
|
488
|
+
if (tabs(l.content).length > cw) wrapCandidates++;
|
|
489
|
+
}
|
|
490
|
+
if (contentLines === 0) return true;
|
|
491
|
+
|
|
492
|
+
const wrapRatio = wrapCandidates / contentLines;
|
|
493
|
+
if (wrapCandidates >= SPLIT_MAX_WRAP_LINES) return false;
|
|
494
|
+
if (wrapRatio >= SPLIT_MAX_WRAP_RATIO) return false;
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// Unified (stacked) view
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
export async function renderUnified(
|
|
503
|
+
diff: ParsedDiff,
|
|
504
|
+
language: BundledLanguage | undefined,
|
|
505
|
+
max = MAX_RENDER_LINES,
|
|
506
|
+
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
507
|
+
): Promise<string> {
|
|
508
|
+
if (!diff.lines.length) return "";
|
|
509
|
+
|
|
510
|
+
const vis = diff.lines.slice(0, max);
|
|
511
|
+
const tw = termW();
|
|
512
|
+
const nw = Math.max(
|
|
513
|
+
2,
|
|
514
|
+
String(Math.max(...vis.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length,
|
|
515
|
+
);
|
|
516
|
+
const gw = nw + 5;
|
|
517
|
+
const cw = Math.max(20, tw - gw);
|
|
518
|
+
const canHL = diff.chars <= MAX_HL_CHARS && vis.length <= MAX_RENDER_LINES;
|
|
519
|
+
|
|
520
|
+
const oldSrc: string[] = [];
|
|
521
|
+
const newSrc: string[] = [];
|
|
522
|
+
for (const l of vis) {
|
|
523
|
+
if (l.type === "ctx" || l.type === "del") oldSrc.push(l.content);
|
|
524
|
+
if (l.type === "ctx" || l.type === "add") newSrc.push(l.content);
|
|
525
|
+
}
|
|
526
|
+
const [oldHL, newHL] = canHL
|
|
527
|
+
? await Promise.all([
|
|
528
|
+
hlBlock(oldSrc.join("\n"), language),
|
|
529
|
+
hlBlock(newSrc.join("\n"), language),
|
|
530
|
+
])
|
|
531
|
+
: [oldSrc, newSrc];
|
|
532
|
+
|
|
533
|
+
let oI = 0;
|
|
534
|
+
let nI = 0;
|
|
535
|
+
let idx = 0;
|
|
536
|
+
const out: string[] = [];
|
|
537
|
+
out.push(rule(tw));
|
|
538
|
+
|
|
539
|
+
function emitRow(
|
|
540
|
+
num: number | null,
|
|
541
|
+
sign: string,
|
|
542
|
+
gutterBg: string,
|
|
543
|
+
signFg: string,
|
|
544
|
+
body: string,
|
|
545
|
+
bodyBg = "",
|
|
546
|
+
): void {
|
|
547
|
+
const borderFg = sign === "-" ? dc.fgDel : sign === "+" ? dc.fgAdd : "";
|
|
548
|
+
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : `${BG_BASE} `;
|
|
549
|
+
const numFg = borderFg || FG_LNUM;
|
|
550
|
+
const gutter = `${border}${gutterBg}${lnum(num, nw, numFg)}${signFg}${sign}${RST} ${DIVIDER} `;
|
|
551
|
+
const contGutter = `${border}${gutterBg}${" ".repeat(nw + 1)}${RST} ${DIVIDER} `;
|
|
552
|
+
const rows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), bodyBg);
|
|
553
|
+
out.push(`${gutter}${rows[0]}${RST}`);
|
|
554
|
+
for (let r = 1; r < rows.length; r++)
|
|
555
|
+
out.push(`${contGutter}${rows[r]}${RST}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
while (idx < vis.length) {
|
|
559
|
+
const l = vis[idx];
|
|
560
|
+
|
|
561
|
+
if (l.type === "sep") {
|
|
562
|
+
const gap = l.newNum;
|
|
563
|
+
const label = gap && gap > 0 ? ` ${gap} unmodified lines ` : "···";
|
|
564
|
+
const totalW = Math.min(tw, 72);
|
|
565
|
+
const pad = Math.max(0, totalW - label.length - 2);
|
|
566
|
+
const half1 = Math.floor(pad / 2);
|
|
567
|
+
const half2 = pad - half1;
|
|
568
|
+
out.push(
|
|
569
|
+
`${BG_BASE}${FG_DIM}${"─".repeat(half1)}${label}${"─".repeat(half2)}${RST}`,
|
|
570
|
+
);
|
|
571
|
+
idx++;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (l.type === "ctx") {
|
|
576
|
+
const hl = oldHL[oI] ?? l.content;
|
|
577
|
+
emitRow(
|
|
578
|
+
l.newNum,
|
|
579
|
+
" ",
|
|
580
|
+
BG_BASE,
|
|
581
|
+
dc.fgCtx,
|
|
582
|
+
`${BG_BASE}${DIM}${hl}`,
|
|
583
|
+
BG_BASE,
|
|
584
|
+
);
|
|
585
|
+
oI++;
|
|
586
|
+
nI++;
|
|
587
|
+
idx++;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const dels: Array<{ l: DiffLine; hl: string }> = [];
|
|
592
|
+
while (idx < vis.length && vis[idx].type === "del") {
|
|
593
|
+
dels.push({ l: vis[idx], hl: oldHL[oI] ?? vis[idx].content });
|
|
594
|
+
oI++;
|
|
595
|
+
idx++;
|
|
596
|
+
}
|
|
597
|
+
const adds: Array<{ l: DiffLine; hl: string }> = [];
|
|
598
|
+
while (idx < vis.length && vis[idx].type === "add") {
|
|
599
|
+
adds.push({ l: vis[idx], hl: newHL[nI] ?? vis[idx].content });
|
|
600
|
+
nI++;
|
|
601
|
+
idx++;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const isPaired = dels.length === 1 && adds.length === 1;
|
|
605
|
+
const wd = isPaired
|
|
606
|
+
? wordDiffAnalysis(dels[0].l.content, adds[0].l.content)
|
|
607
|
+
: null;
|
|
608
|
+
const wdBalanced = wd && wd.oldRanges.length > 0 && wd.newRanges.length > 0;
|
|
609
|
+
|
|
610
|
+
if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
|
|
611
|
+
const delBody = injectBg(dels[0].hl, wd.oldRanges, BG_DEL, BG_DEL_W);
|
|
612
|
+
const addBody = injectBg(adds[0].hl, wd.newRanges, BG_ADD, BG_ADD_W);
|
|
613
|
+
emitRow(
|
|
614
|
+
dels[0].l.oldNum,
|
|
615
|
+
"-",
|
|
616
|
+
BG_GUTTER_DEL,
|
|
617
|
+
`${dc.fgDel}${BOLD}`,
|
|
618
|
+
delBody,
|
|
619
|
+
BG_DEL,
|
|
620
|
+
);
|
|
621
|
+
emitRow(
|
|
622
|
+
adds[0].l.newNum,
|
|
623
|
+
"+",
|
|
624
|
+
BG_GUTTER_ADD,
|
|
625
|
+
`${dc.fgAdd}${BOLD}`,
|
|
626
|
+
addBody,
|
|
627
|
+
BG_ADD,
|
|
628
|
+
);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (
|
|
632
|
+
isPaired &&
|
|
633
|
+
wdBalanced &&
|
|
634
|
+
wd.similarity >= WORD_DIFF_MIN_SIM &&
|
|
635
|
+
!canHL
|
|
636
|
+
) {
|
|
637
|
+
const pwd = plainWordDiff(dels[0].l.content, adds[0].l.content);
|
|
638
|
+
emitRow(
|
|
639
|
+
dels[0].l.oldNum,
|
|
640
|
+
"-",
|
|
641
|
+
BG_GUTTER_DEL,
|
|
642
|
+
`${dc.fgDel}${BOLD}`,
|
|
643
|
+
`${BG_DEL}${pwd.old}`,
|
|
644
|
+
BG_DEL,
|
|
645
|
+
);
|
|
646
|
+
emitRow(
|
|
647
|
+
adds[0].l.newNum,
|
|
648
|
+
"+",
|
|
649
|
+
BG_GUTTER_ADD,
|
|
650
|
+
`${dc.fgAdd}${BOLD}`,
|
|
651
|
+
`${BG_ADD}${pwd.new}`,
|
|
652
|
+
BG_ADD,
|
|
653
|
+
);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
for (const d of dels) {
|
|
658
|
+
const body = canHL ? `${BG_DEL}${d.hl}` : `${BG_DEL}${d.l.content}`;
|
|
659
|
+
emitRow(
|
|
660
|
+
d.l.oldNum,
|
|
661
|
+
"-",
|
|
662
|
+
BG_GUTTER_DEL,
|
|
663
|
+
`${dc.fgDel}${BOLD}`,
|
|
664
|
+
body,
|
|
665
|
+
BG_DEL,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
for (const a of adds) {
|
|
669
|
+
const body = canHL ? `${BG_ADD}${a.hl}` : `${BG_ADD}${a.l.content}`;
|
|
670
|
+
emitRow(
|
|
671
|
+
a.l.newNum,
|
|
672
|
+
"+",
|
|
673
|
+
BG_GUTTER_ADD,
|
|
674
|
+
`${dc.fgAdd}${BOLD}`,
|
|
675
|
+
body,
|
|
676
|
+
BG_ADD,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
out.push(rule(tw));
|
|
682
|
+
if (diff.lines.length > vis.length) {
|
|
683
|
+
out.push(
|
|
684
|
+
`${BG_BASE}${FG_DIM} … ${diff.lines.length - vis.length} more lines${RST}`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return out.join("\n");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
// Split view (auto-fallback to unified when narrow)
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
|
|
694
|
+
export async function renderSplit(
|
|
695
|
+
diff: ParsedDiff,
|
|
696
|
+
language: BundledLanguage | undefined,
|
|
697
|
+
max = MAX_PREVIEW_LINES,
|
|
698
|
+
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
699
|
+
): Promise<string> {
|
|
700
|
+
const tw = termW();
|
|
701
|
+
if (!shouldUseSplit(diff, tw, max))
|
|
702
|
+
return renderUnified(diff, language, max, dc);
|
|
703
|
+
if (!diff.lines.length) return "";
|
|
704
|
+
|
|
705
|
+
type Row = { left: DiffLine | null; right: DiffLine | null };
|
|
706
|
+
const rows: Row[] = [];
|
|
707
|
+
let i = 0;
|
|
708
|
+
while (i < diff.lines.length) {
|
|
709
|
+
const l = diff.lines[i];
|
|
710
|
+
if (l.type === "sep" || l.type === "ctx") {
|
|
711
|
+
rows.push({ left: l, right: l });
|
|
712
|
+
i++;
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const dels: DiffLine[] = [];
|
|
716
|
+
const adds: DiffLine[] = [];
|
|
717
|
+
while (i < diff.lines.length && diff.lines[i].type === "del") {
|
|
718
|
+
dels.push(diff.lines[i]);
|
|
719
|
+
i++;
|
|
720
|
+
}
|
|
721
|
+
while (i < diff.lines.length && diff.lines[i].type === "add") {
|
|
722
|
+
adds.push(diff.lines[i]);
|
|
723
|
+
i++;
|
|
724
|
+
}
|
|
725
|
+
const n = Math.max(dels.length, adds.length);
|
|
726
|
+
for (let j = 0; j < n; j++)
|
|
727
|
+
rows.push({ left: dels[j] ?? null, right: adds[j] ?? null });
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const vis = rows.slice(0, max);
|
|
731
|
+
const half = Math.floor((tw - 1) / 2);
|
|
732
|
+
const nw = Math.max(
|
|
733
|
+
2,
|
|
734
|
+
String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0))
|
|
735
|
+
.length,
|
|
736
|
+
);
|
|
737
|
+
const gw = nw + 5;
|
|
738
|
+
const cw = Math.max(12, half - gw);
|
|
739
|
+
const canHL =
|
|
740
|
+
diff.chars <= MAX_HL_CHARS && vis.length * 2 <= MAX_RENDER_LINES * 2;
|
|
741
|
+
|
|
742
|
+
const leftSrc: string[] = [];
|
|
743
|
+
const rightSrc: string[] = [];
|
|
744
|
+
for (const r of vis) {
|
|
745
|
+
if (r.left && r.left.type !== "sep") leftSrc.push(r.left.content);
|
|
746
|
+
if (r.right && r.right.type !== "sep") rightSrc.push(r.right.content);
|
|
747
|
+
}
|
|
748
|
+
const [leftHL, rightHL] = canHL
|
|
749
|
+
? await Promise.all([
|
|
750
|
+
hlBlock(leftSrc.join("\n"), language),
|
|
751
|
+
hlBlock(rightSrc.join("\n"), language),
|
|
752
|
+
])
|
|
753
|
+
: [leftSrc, rightSrc];
|
|
754
|
+
|
|
755
|
+
let lI = 0;
|
|
756
|
+
let rI = 0;
|
|
757
|
+
let stripeRow = 0;
|
|
758
|
+
|
|
759
|
+
type HalfResult = { gutter: string; contGutter: string; bodyRows: string[] };
|
|
760
|
+
|
|
761
|
+
function half_build(
|
|
762
|
+
line: DiffLine | null,
|
|
763
|
+
hl: string,
|
|
764
|
+
ranges: Array<[number, number]> | null,
|
|
765
|
+
side: "left" | "right",
|
|
766
|
+
): HalfResult {
|
|
767
|
+
if (!line) {
|
|
768
|
+
const gw2 = nw + 2;
|
|
769
|
+
const gPat = FG_STRIPE + "╱".repeat(gw2) + RST;
|
|
770
|
+
const g = ` ${gPat}${FG_RULE}│${RST} `;
|
|
771
|
+
return { gutter: g, contGutter: g, bodyRows: [stripes(cw, stripeRow)] };
|
|
772
|
+
}
|
|
773
|
+
if (line.type === "sep") {
|
|
774
|
+
const gap = line.newNum;
|
|
775
|
+
const label = gap && gap > 0 ? `··· ${gap} lines ···` : "···";
|
|
776
|
+
const g = `${BG_BASE} ${FG_DIM}${fit("", nw + 2)}${RST}${FG_RULE}│${RST} `;
|
|
777
|
+
return {
|
|
778
|
+
gutter: g,
|
|
779
|
+
contGutter: g,
|
|
780
|
+
bodyRows: [`${BG_BASE}${FG_DIM}${fit(label, cw)}${RST}`],
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const isDel = line.type === "del";
|
|
785
|
+
const isAdd = line.type === "add";
|
|
786
|
+
const gBg = isDel ? BG_GUTTER_DEL : isAdd ? BG_GUTTER_ADD : BG_BASE;
|
|
787
|
+
const cBg = isDel ? BG_DEL : isAdd ? BG_ADD : BG_BASE;
|
|
788
|
+
const sFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : dc.fgCtx;
|
|
789
|
+
const sign = isDel ? "-" : isAdd ? "+" : " ";
|
|
790
|
+
const num = isDel
|
|
791
|
+
? line.oldNum
|
|
792
|
+
: isAdd
|
|
793
|
+
? line.newNum
|
|
794
|
+
: side === "left"
|
|
795
|
+
? line.oldNum
|
|
796
|
+
: line.newNum;
|
|
797
|
+
|
|
798
|
+
const borderFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : "";
|
|
799
|
+
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : ` ${BG_BASE}`;
|
|
800
|
+
const numFg = borderFg || FG_LNUM;
|
|
801
|
+
|
|
802
|
+
let body: string;
|
|
803
|
+
if (ranges && ranges.length > 0) {
|
|
804
|
+
body = injectBg(hl, ranges, cBg, isDel ? BG_DEL_W : BG_ADD_W);
|
|
805
|
+
} else if (isDel || isAdd) {
|
|
806
|
+
body = `${cBg}${hl}`;
|
|
807
|
+
} else {
|
|
808
|
+
body = `${BG_BASE}${DIM}${hl}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const gutter = `${border}${gBg}${lnum(num, nw, numFg)}${sFg}${BOLD}${sign}${RST} ${FG_RULE}│${RST} `;
|
|
812
|
+
const contGutter = `${border}${gBg}${" ".repeat(nw + 1)}${RST} ${FG_RULE}│${RST} `;
|
|
813
|
+
const bodyRows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), cBg);
|
|
814
|
+
return { gutter, contGutter, bodyRows };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const out: string[] = [];
|
|
818
|
+
const hdrOld = `${BG_BASE}${" ".repeat(Math.max(0, nw - 2))}${dc.fgDel}${DIM}old${RST}`;
|
|
819
|
+
const hdrNew = `${BG_BASE}${" ".repeat(Math.max(0, nw - 2))}${dc.fgAdd}${DIM}new${RST}`;
|
|
820
|
+
out.push(
|
|
821
|
+
`${BG_BASE}${hdrOld}${" ".repeat(Math.max(0, half - nw - 1))}${FG_RULE}┊${RST}${hdrNew}`,
|
|
822
|
+
);
|
|
823
|
+
out.push(`${rule(half)}${FG_RULE}┊${RST}${rule(half)}`);
|
|
824
|
+
|
|
825
|
+
for (const r of vis) {
|
|
826
|
+
const leftLine = r.left;
|
|
827
|
+
const rightLine = r.right;
|
|
828
|
+
const paired =
|
|
829
|
+
leftLine &&
|
|
830
|
+
rightLine &&
|
|
831
|
+
leftLine.type === "del" &&
|
|
832
|
+
rightLine.type === "add";
|
|
833
|
+
const wd = paired
|
|
834
|
+
? wordDiffAnalysis(leftLine.content, rightLine.content)
|
|
835
|
+
: null;
|
|
836
|
+
|
|
837
|
+
let lResult: HalfResult;
|
|
838
|
+
let rResult: HalfResult;
|
|
839
|
+
|
|
840
|
+
if (paired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
|
|
841
|
+
const lhl = leftHL[lI++] ?? leftLine.content;
|
|
842
|
+
const rhl = rightHL[rI++] ?? rightLine.content;
|
|
843
|
+
lResult = half_build(leftLine, lhl, wd.oldRanges, "left");
|
|
844
|
+
rResult = half_build(rightLine, rhl, wd.newRanges, "right");
|
|
845
|
+
} else if (paired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
|
|
846
|
+
const pwd = plainWordDiff(leftLine.content, rightLine.content);
|
|
847
|
+
lI++;
|
|
848
|
+
rI++;
|
|
849
|
+
lResult = half_build(leftLine, pwd.old, null, "left");
|
|
850
|
+
rResult = half_build(rightLine, pwd.new, null, "right");
|
|
851
|
+
} else {
|
|
852
|
+
const lhl =
|
|
853
|
+
leftLine && leftLine.type !== "sep"
|
|
854
|
+
? (leftHL[lI++] ?? leftLine?.content ?? "")
|
|
855
|
+
: "";
|
|
856
|
+
const rhl =
|
|
857
|
+
rightLine && rightLine.type !== "sep"
|
|
858
|
+
? (rightHL[rI++] ?? rightLine?.content ?? "")
|
|
859
|
+
: "";
|
|
860
|
+
lResult = half_build(leftLine, lhl, null, "left");
|
|
861
|
+
rResult = half_build(rightLine, rhl, null, "right");
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const maxRowsN = Math.max(lResult.bodyRows.length, rResult.bodyRows.length);
|
|
865
|
+
const leftIsEmpty = !r.left;
|
|
866
|
+
const rightIsEmpty = !r.right;
|
|
867
|
+
for (let row = 0; row < maxRowsN; row++) {
|
|
868
|
+
const lg = row === 0 ? lResult.gutter : lResult.contGutter;
|
|
869
|
+
const rg = row === 0 ? rResult.gutter : rResult.contGutter;
|
|
870
|
+
const lb =
|
|
871
|
+
lResult.bodyRows[row] ??
|
|
872
|
+
(leftIsEmpty
|
|
873
|
+
? stripes(cw, stripeRow)
|
|
874
|
+
: `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
|
|
875
|
+
const rb =
|
|
876
|
+
rResult.bodyRows[row] ??
|
|
877
|
+
(rightIsEmpty
|
|
878
|
+
? stripes(cw, stripeRow)
|
|
879
|
+
: `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
|
|
880
|
+
out.push(`${lg}${lb}${DIVIDER}${rg}${rb}`);
|
|
881
|
+
stripeRow++;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
out.push(`${rule(half)}${FG_RULE}┊${RST}${rule(half)}`);
|
|
886
|
+
if (rows.length > vis.length) {
|
|
887
|
+
out.push(
|
|
888
|
+
`${BG_BASE}${FG_DIM} … ${rows.length - vis.length} more lines${RST}`,
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return out.join("\n");
|
|
892
|
+
}
|