@pi-archimedes/diff 0.3.2 → 0.5.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/package.json +1 -1
- package/src/ansi.ts +25 -5
- package/src/core/diff.ts +5 -4
- package/src/index.ts +3 -10
- package/src/render.ts +18 -18
- package/src/shiki.ts +5 -4
- package/src/word-diff.ts +19 -4
package/package.json
CHANGED
package/src/ansi.ts
CHANGED
|
@@ -103,7 +103,7 @@ export function isLowContrastShikiFg(params: string): boolean {
|
|
|
103
103
|
if (!params.startsWith("38;2;")) return false;
|
|
104
104
|
const parts = params.split(";").map(Number);
|
|
105
105
|
if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n))) return false;
|
|
106
|
-
const [, , r, g, b] = parts;
|
|
106
|
+
const [, , r, g, b] = parts as [number, number, number, number, number];
|
|
107
107
|
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
108
108
|
return luminance < 72;
|
|
109
109
|
}
|
|
@@ -156,7 +156,7 @@ export function summarize(a: number, d: number): string {
|
|
|
156
156
|
function parseAnsiRgb(ansi: string): { r: number; g: number; b: number } | null {
|
|
157
157
|
const esc = "\u001b";
|
|
158
158
|
const m = ansi.match(new RegExp(`${esc}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`));
|
|
159
|
-
return m ? { r: +m[1]
|
|
159
|
+
return m ? { r: +m[1]!, g: +m[2]!, b: +m[3]! } : null;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/** Mix an accent color into a base color at the given intensity (0.0–1.0). */
|
|
@@ -243,11 +243,31 @@ export function themeCacheKey(theme?: any): string {
|
|
|
243
243
|
return parts.join("|");
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
let
|
|
246
|
+
let _lastThemeKey: string | undefined;
|
|
247
|
+
|
|
248
|
+
/** Reset auto-derived colors — call when theme changes. */
|
|
249
|
+
export function resetDiffColors(): void {
|
|
250
|
+
_lastThemeKey = undefined;
|
|
251
|
+
// Restore hardcoded fallbacks
|
|
252
|
+
BG_ADD = "\x1b[48;2;22;38;32m";
|
|
253
|
+
BG_DEL = "\x1b[48;2;45;25;25m";
|
|
254
|
+
BG_ADD_W = "\x1b[48;2;35;75;50m";
|
|
255
|
+
BG_DEL_W = "\x1b[48;2;80;35;35m";
|
|
256
|
+
BG_GUTTER_ADD = "\x1b[48;2;18;32;26m";
|
|
257
|
+
BG_GUTTER_DEL = "\x1b[48;2;38;22;22m";
|
|
258
|
+
BG_EMPTY = "\x1b[48;2;18;18;18m";
|
|
259
|
+
BG_BASE = BG_DEFAULT;
|
|
260
|
+
RST = "\x1b[0m";
|
|
261
|
+
DIVIDER = `${FG_RULE}│${RST}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
247
264
|
export function resolveDiffColors(theme?: any): DiffColors {
|
|
248
|
-
|
|
265
|
+
const themeKey = themeCacheKey(theme);
|
|
266
|
+
|
|
267
|
+
// Re-derive when theme changes (different key) or first call
|
|
268
|
+
if (themeKey !== _lastThemeKey && theme?.getFgAnsi) {
|
|
249
269
|
autoDeriveBgFromTheme(theme);
|
|
250
|
-
|
|
270
|
+
_lastThemeKey = themeKey;
|
|
251
271
|
}
|
|
252
272
|
if (!theme?.getFgAnsi) return DEFAULT_DIFF_COLORS;
|
|
253
273
|
try {
|
package/src/core/diff.ts
CHANGED
|
@@ -22,16 +22,17 @@ export function parseDiff(oldContent: string, newContent: string, ctx = 3): Pars
|
|
|
22
22
|
|
|
23
23
|
for (let hi = 0; hi < patch.hunks.length; hi++) {
|
|
24
24
|
if (hi > 0) {
|
|
25
|
-
const prev = patch.hunks[hi - 1]
|
|
26
|
-
const
|
|
25
|
+
const prev = patch.hunks[hi - 1]!;
|
|
26
|
+
const curr = patch.hunks[hi]!;
|
|
27
|
+
const gap = curr.oldStart - (prev.oldStart + prev.oldLines);
|
|
27
28
|
lines.push({ type: "sep", oldNum: null, newNum: gap > 0 ? gap : null, content: "" });
|
|
28
29
|
}
|
|
29
|
-
const h = patch.hunks[hi]
|
|
30
|
+
const h = patch.hunks[hi]!;
|
|
30
31
|
let oL = h.oldStart;
|
|
31
32
|
let nL = h.newStart;
|
|
32
33
|
for (const raw of h.lines) {
|
|
33
34
|
if (raw === "\") continue;
|
|
34
|
-
const ch = raw[0]
|
|
35
|
+
const ch = raw[0]!;
|
|
35
36
|
const text = raw.slice(1);
|
|
36
37
|
if (ch === "+") {
|
|
37
38
|
lines.push({ type: "add", oldNum: null, newNum: nL++, content: text });
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import * as Ansi from "./ansi.js";
|
|
|
13
13
|
import { resolveDiffColors, themeCacheKey, DEFAULT_DIFF_COLORS } from "./ansi.js";
|
|
14
14
|
import { setConfigGetter as setShikiConfig } from "./shiki.js";
|
|
15
15
|
import { hlBlock, lang } from "./shiki.js";
|
|
16
|
-
import { renderSplit } from "./render.js";
|
|
16
|
+
import { renderSplit, MAX_PREVIEW_LINES, MAX_RENDER_LINES } from "./render.js";
|
|
17
17
|
import { setConfigGetter as setRenderConfig } from "./render.js";
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
@@ -35,13 +35,6 @@ const DEFAULT_DIFF_CONFIG: DiffConfig = {
|
|
|
35
35
|
let _readConfig: () => DiffConfig = () => DEFAULT_DIFF_CONFIG;
|
|
36
36
|
function getConfig(): DiffConfig { return _readConfig(); }
|
|
37
37
|
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Constants
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
const MAX_PREVIEW_LINES = 60;
|
|
43
|
-
const MAX_RENDER_LINES = 150;
|
|
44
|
-
|
|
45
38
|
// ---------------------------------------------------------------------------
|
|
46
39
|
// Settings
|
|
47
40
|
// ---------------------------------------------------------------------------
|
|
@@ -296,7 +289,7 @@ export function registerDiffTools(
|
|
|
296
289
|
try {
|
|
297
290
|
if (fp && existsSync(fp)) {
|
|
298
291
|
const f = readFileSync(fp, "utf-8");
|
|
299
|
-
const idx = f.indexOf(operations[0]
|
|
292
|
+
const idx = f.indexOf(operations[0]!.newText);
|
|
300
293
|
if (idx >= 0) editLine = f.slice(0, idx).split("\n").length;
|
|
301
294
|
}
|
|
302
295
|
} catch { editLine = 0; }
|
|
@@ -332,7 +325,7 @@ export function registerDiffTools(
|
|
|
332
325
|
const dc = resolveDiffColors(theme);
|
|
333
326
|
|
|
334
327
|
if (operations.length === 1) {
|
|
335
|
-
const diff = parseDiff(operations[0]
|
|
328
|
+
const diff = parseDiff(operations[0]!.oldText, operations[0]!.newText);
|
|
336
329
|
renderSplit(diff, lg, MAX_PREVIEW_LINES, dc)
|
|
337
330
|
.then((rendered) => {
|
|
338
331
|
if (ctx.state._pk !== pk) return;
|
package/src/render.ts
CHANGED
|
@@ -12,8 +12,8 @@ import type { DiffLine, ParsedDiff } from "./core/diff.js";
|
|
|
12
12
|
// Constants
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
-
const MAX_PREVIEW_LINES = 60;
|
|
16
|
-
const MAX_RENDER_LINES = 150;
|
|
15
|
+
export const MAX_PREVIEW_LINES = 60;
|
|
16
|
+
export const MAX_RENDER_LINES = 150;
|
|
17
17
|
const MAX_HL_CHARS = 80_000;
|
|
18
18
|
const WORD_DIFF_MIN_SIM = 0.15;
|
|
19
19
|
const SPLIT_MAX_WRAP_RATIO = 0.2;
|
|
@@ -187,7 +187,7 @@ export async function renderUnified(
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
while (idx < vis.length) {
|
|
190
|
-
const l = vis[idx]
|
|
190
|
+
const l = vis[idx]!;
|
|
191
191
|
|
|
192
192
|
if (l.type === "sep") {
|
|
193
193
|
const gap = l.newNum;
|
|
@@ -208,30 +208,30 @@ export async function renderUnified(
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
const dels: Array<{ l: DiffLine; hl: string }> = [];
|
|
211
|
-
while (idx < vis.length && vis[idx]
|
|
212
|
-
dels.push({ l: vis[idx]
|
|
211
|
+
while (idx < vis.length && vis[idx]!.type === "del") {
|
|
212
|
+
dels.push({ l: vis[idx]!, hl: oldHL[oI] ?? vis[idx]!.content });
|
|
213
213
|
oI++; idx++;
|
|
214
214
|
}
|
|
215
215
|
const adds: Array<{ l: DiffLine; hl: string }> = [];
|
|
216
|
-
while (idx < vis.length && vis[idx]
|
|
217
|
-
adds.push({ l: vis[idx]
|
|
216
|
+
while (idx < vis.length && vis[idx]!.type === "add") {
|
|
217
|
+
adds.push({ l: vis[idx]!, hl: newHL[nI] ?? vis[idx]!.content });
|
|
218
218
|
nI++; idx++;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
const isPaired = dels.length === 1 && adds.length === 1;
|
|
222
|
-
const wd = isPaired ? wordDiffAnalysis(dels[0]
|
|
222
|
+
const wd = isPaired ? wordDiffAnalysis(dels[0]!.l.content, adds[0]!.l.content) : null;
|
|
223
223
|
|
|
224
224
|
if (isPaired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
|
|
225
|
-
const delBody = injectBg(dels[0]
|
|
226
|
-
const addBody = injectBg(adds[0]
|
|
227
|
-
emitRow(dels[0]
|
|
228
|
-
emitRow(adds[0]
|
|
225
|
+
const delBody = injectBg(dels[0]!.hl, wd.oldRanges, Ansi.BG_DEL, Ansi.BG_DEL_W);
|
|
226
|
+
const addBody = injectBg(adds[0]!.hl, wd.newRanges, Ansi.BG_ADD, Ansi.BG_ADD_W);
|
|
227
|
+
emitRow(dels[0]!.l.oldNum, "-", Ansi.BG_GUTTER_DEL, `${dc.fgDel}${Ansi.BOLD}`, delBody, Ansi.BG_DEL);
|
|
228
|
+
emitRow(adds[0]!.l.newNum, "+", Ansi.BG_GUTTER_ADD, `${dc.fgAdd}${Ansi.BOLD}`, addBody, Ansi.BG_ADD);
|
|
229
229
|
continue;
|
|
230
230
|
}
|
|
231
231
|
if (isPaired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
|
|
232
|
-
const pwd = plainWordDiff(dels[0]
|
|
233
|
-
emitRow(dels[0]
|
|
234
|
-
emitRow(adds[0]
|
|
232
|
+
const pwd = plainWordDiff(dels[0]!.l.content, adds[0]!.l.content);
|
|
233
|
+
emitRow(dels[0]!.l.oldNum, "-", Ansi.BG_GUTTER_DEL, `${dc.fgDel}${Ansi.BOLD}`, `${Ansi.BG_DEL}${pwd.old}`, Ansi.BG_DEL);
|
|
234
|
+
emitRow(adds[0]!.l.newNum, "+", Ansi.BG_GUTTER_ADD, `${dc.fgAdd}${Ansi.BOLD}`, `${Ansi.BG_ADD}${pwd.new}`, Ansi.BG_ADD);
|
|
235
235
|
continue;
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -270,11 +270,11 @@ export async function renderSplit(
|
|
|
270
270
|
const rows: Row[] = [];
|
|
271
271
|
let i = 0;
|
|
272
272
|
while (i < diff.lines.length) {
|
|
273
|
-
const l = diff.lines[i]
|
|
273
|
+
const l = diff.lines[i]!;
|
|
274
274
|
if (l.type === "sep" || l.type === "ctx") { rows.push({ left: l, right: l }); i++; continue; }
|
|
275
275
|
const dels: DiffLine[] = [], adds: DiffLine[] = [];
|
|
276
|
-
while (i < diff.lines.length && diff.lines[i]
|
|
277
|
-
while (i < diff.lines.length && diff.lines[i]
|
|
276
|
+
while (i < diff.lines.length && diff.lines[i]!.type === "del") { dels.push(diff.lines[i]!); i++; }
|
|
277
|
+
while (i < diff.lines.length && diff.lines[i]!.type === "add") { adds.push(diff.lines[i]!); i++; }
|
|
278
278
|
const n = Math.max(dels.length, adds.length);
|
|
279
279
|
for (let j = 0; j < n; j++) rows.push({ left: dels[j] ?? null, right: adds[j] ?? null });
|
|
280
280
|
}
|
package/src/shiki.ts
CHANGED
|
@@ -52,12 +52,13 @@ async function ensureShiki(): Promise<void> {
|
|
|
52
52
|
const _cache = new Map<string, string[]>();
|
|
53
53
|
|
|
54
54
|
function _touch(k: string, v: string[]): string[] {
|
|
55
|
+
// True LRU: delete and re-insert to move to end (most recently used)
|
|
55
56
|
_cache.delete(k);
|
|
56
57
|
_cache.set(k, v);
|
|
57
58
|
while (_cache.size > CACHE_LIMIT) {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
_cache.delete(
|
|
59
|
+
const oldest = _cache.keys().next().value;
|
|
60
|
+
if (oldest === undefined) break;
|
|
61
|
+
_cache.delete(oldest);
|
|
61
62
|
}
|
|
62
63
|
return v;
|
|
63
64
|
}
|
|
@@ -71,7 +72,7 @@ export async function hlBlock(code: string, language: BundledLanguage | undefine
|
|
|
71
72
|
const theme = _getConfig().diffTheme;
|
|
72
73
|
const k = `${theme}\0${language}\0${code}`;
|
|
73
74
|
const hit = _cache.get(k);
|
|
74
|
-
if (hit) return _touch(k, hit);
|
|
75
|
+
if (hit) return _touch(k, hit); // Promote to most-recently-used
|
|
75
76
|
|
|
76
77
|
try {
|
|
77
78
|
const ansi = normalizeShikiContrast(await codeToANSI(code, language, theme as BundledTheme));
|
package/src/word-diff.ts
CHANGED
|
@@ -52,6 +52,8 @@ export function wordDiffAnalysis(
|
|
|
52
52
|
* Inject diff background into Shiki ANSI output.
|
|
53
53
|
* `baseBg` on unchanged spans, `hlBg` on changed character ranges.
|
|
54
54
|
* Re-injects bg after any full reset (\x1b[0m).
|
|
55
|
+
*
|
|
56
|
+
* Handles unsorted and overlapping ranges by checking all ranges per position.
|
|
55
57
|
*/
|
|
56
58
|
export function injectBg(
|
|
57
59
|
ansiLine: string,
|
|
@@ -61,6 +63,17 @@ export function injectBg(
|
|
|
61
63
|
): string {
|
|
62
64
|
if (!ranges.length) return baseBg + ansiLine + Ansi.RST;
|
|
63
65
|
|
|
66
|
+
// Sort ranges and merge overlaps for efficient lookup
|
|
67
|
+
const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
|
|
68
|
+
const merged: Array<[number, number]> = [];
|
|
69
|
+
for (const [start, end] of sorted) {
|
|
70
|
+
if (merged.length > 0 && start <= merged[merged.length - 1]![1]) {
|
|
71
|
+
merged[merged.length - 1]![1] = Math.max(merged[merged.length - 1]![1], end);
|
|
72
|
+
} else {
|
|
73
|
+
merged.push([start, end]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
let out = baseBg;
|
|
65
78
|
let vis = 0;
|
|
66
79
|
let inHL = false;
|
|
@@ -68,7 +81,7 @@ export function injectBg(
|
|
|
68
81
|
let i = 0;
|
|
69
82
|
|
|
70
83
|
while (i < ansiLine.length) {
|
|
71
|
-
if (ansiLine[i] === "\x1b") {
|
|
84
|
+
if (ansiLine[i]! === "\x1b") {
|
|
72
85
|
const m = ansiLine.indexOf("m", i);
|
|
73
86
|
if (m !== -1) {
|
|
74
87
|
const seq = ansiLine.slice(i, m + 1);
|
|
@@ -78,13 +91,15 @@ export function injectBg(
|
|
|
78
91
|
continue;
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
// Advance range index past completed ranges
|
|
95
|
+
while (ri < merged.length && vis >= merged[ri]![1]) ri++;
|
|
96
|
+
// Check if current position falls within the next range
|
|
97
|
+
const want = ri < merged.length && vis >= merged[ri]![0] && vis < merged[ri]![1];
|
|
83
98
|
if (want !== inHL) {
|
|
84
99
|
inHL = want;
|
|
85
100
|
out += inHL ? hlBg : baseBg;
|
|
86
101
|
}
|
|
87
|
-
out += ansiLine[i]
|
|
102
|
+
out += ansiLine[i]!;
|
|
88
103
|
vis++;
|
|
89
104
|
i++;
|
|
90
105
|
}
|