@heyhuynhgiabuu/pi-diff 0.4.0 → 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/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * pi-diff — Shiki-powered terminal diff renderer for pi.
4
3
  *
@@ -21,52 +20,16 @@
21
20
  * • Large-diff fallback (skip highlighting, still show diff)
22
21
  * • Async rendering with invalidate() for non-blocking preview
23
22
  */
24
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
- if (k2 === undefined) k2 = k;
26
- var desc = Object.getOwnPropertyDescriptor(m, k);
27
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
- desc = { enumerable: true, get: function() { return m[k]; } };
29
- }
30
- Object.defineProperty(o, k2, desc);
31
- }) : (function(o, m, k, k2) {
32
- if (k2 === undefined) k2 = k;
33
- o[k2] = m[k];
34
- }));
35
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
- Object.defineProperty(o, "default", { enumerable: true, value: v });
37
- }) : function(o, v) {
38
- o["default"] = v;
39
- });
40
- var __importStar = (this && this.__importStar) || (function () {
41
- var ownKeys = function(o) {
42
- ownKeys = Object.getOwnPropertyNames || function (o) {
43
- var ar = [];
44
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
- return ar;
46
- };
47
- return ownKeys(o);
48
- };
49
- return function (mod) {
50
- if (mod && mod.__esModule) return mod;
51
- var result = {};
52
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
- __setModuleDefault(result, mod);
54
- return result;
55
- };
56
- })();
57
- Object.defineProperty(exports, "__esModule", { value: true });
58
- exports.__testing = void 0;
59
- exports.default = diffRendererExtension;
60
- const node_fs_1 = require("node:fs");
61
- const node_path_1 = require("node:path");
62
- const cli_1 = require("@shikijs/cli");
63
- const Diff = __importStar(require("diff"));
64
- const diff_js_1 = require("./core/diff.js");
65
- const command_js_1 = require("./review/command.js");
66
- const export_js_1 = require("./review/export.js");
67
- const git_js_1 = require("./review/git.js");
68
- const hunk_preview_js_1 = require("./review/hunk-preview.js");
69
- const interactive_js_1 = require("./review/interactive.js");
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import { extname, relative } from "node:path";
25
+ import { codeToANSI } from "@shikijs/cli";
26
+ import * as Diff from "diff";
27
+ import { parseDiff } from "./core/diff.js";
28
+ import { registerReviewDiffCommand } from "./review/command.js";
29
+ import { formatReviewMarkdown } from "./review/export.js";
30
+ import { countReviewDiffLines, readGitDiff } from "./review/git.js";
31
+ import { applyDiffPalette as applySharedDiffPalette, lang as detectDiffLanguage, renderSplit as renderSharedSplit, resolveDiffColors as resolveSharedDiffColors, themeCacheKey as sharedThemeCacheKey, } from "./review/hunk-preview.js";
32
+ import { formatInteractiveReviewPanel } from "./review/interactive.js";
70
33
  const DIFF_PRESETS = {
71
34
  default: {
72
35
  name: "default",
@@ -245,8 +208,8 @@ function loadDiffConfig() {
245
208
  const paths = [`${process.cwd()}/.pi/settings.json`, `${process.env.HOME ?? ""}/.pi/settings.json`];
246
209
  for (const p of paths) {
247
210
  try {
248
- if ((0, node_fs_1.existsSync)(p)) {
249
- const raw = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
211
+ if (existsSync(p)) {
212
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
250
213
  if (raw.diffTheme || raw.diffColors) {
251
214
  return { diffTheme: raw.diffTheme, diffColors: raw.diffColors };
252
215
  }
@@ -681,7 +644,7 @@ function lnum(n, w, fg = FG_LNUM) {
681
644
  function shortPath(cwd, home, p) {
682
645
  if (!p)
683
646
  return "";
684
- const r = (0, node_path_1.relative)(cwd, p);
647
+ const r = relative(cwd, p);
685
648
  if (!r.startsWith("..") && !r.startsWith("/"))
686
649
  return r;
687
650
  return p.replace(home, "~");
@@ -777,14 +740,14 @@ const EXT_LANG = {
777
740
  vue: "vue",
778
741
  };
779
742
  function lang(fp) {
780
- return EXT_LANG[(0, node_path_1.extname)(fp).slice(1).toLowerCase()];
743
+ return EXT_LANG[extname(fp).slice(1).toLowerCase()];
781
744
  }
782
745
  // ---------------------------------------------------------------------------
783
746
  // Shiki ANSI cache + pre-warm
784
747
  // ---------------------------------------------------------------------------
785
748
  // Pre-warm the Shiki singleton (loads WASM grammars + theme) so the first
786
749
  // diff render doesn't pay the ~200-500ms startup cost.
787
- (0, cli_1.codeToANSI)("", "typescript", THEME).catch(() => { });
750
+ codeToANSI("", "typescript", THEME).catch(() => { });
788
751
  const _cache = new Map();
789
752
  function _touch(k, v) {
790
753
  _cache.delete(k);
@@ -807,7 +770,7 @@ async function hlBlock(code, language) {
807
770
  if (hit)
808
771
  return _touch(k, hit);
809
772
  try {
810
- const ansi = normalizeShikiContrast(await (0, cli_1.codeToANSI)(code, language, THEME));
773
+ const ansi = normalizeShikiContrast(await codeToANSI(code, language, THEME));
811
774
  const out = (ansi.endsWith("\n") ? ansi.slice(0, -1) : ansi).split("\n");
812
775
  return _touch(k, out);
813
776
  }
@@ -875,9 +838,13 @@ function injectBg(ansiLine, ranges, baseBg, hlBg) {
875
838
  if (m !== -1) {
876
839
  const seq = ansiLine.slice(i, m + 1);
877
840
  out += seq;
878
- // Re-inject bg after full reset
879
- if (seq === "\x1b[0m")
841
+ // Re-inject bg after any reset-like sequence.
842
+ // Shiki uses \x1b[39m (fg reset) between tokens — technically this
843
+ // doesn't clear background per ANSI spec, but some terminal
844
+ // emulators may treat it as a broader reset.
845
+ if (seq === "\x1b[0m" || seq === "\x1b[39m" || seq === "\x1b[49m") {
880
846
  out += inHL ? hlBg : baseBg;
847
+ }
881
848
  i = m + 1;
882
849
  continue;
883
850
  }
@@ -995,14 +962,19 @@ async function renderUnified(diff, language, max = MAX_RENDER_LINES, dc = DEFAUL
995
962
  // 1:1 paired → word diff emphasis
996
963
  const isPaired = dels.length === 1 && adds.length === 1;
997
964
  const wd = isPaired ? wordDiffAnalysis(dels[0].l.content, adds[0].l.content) : null;
998
- if (isPaired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
965
+ // Word-diff emphasis only use when BOTH sides have ranges.
966
+ // When diffWords treats trailing punctuation as "common" while removing
967
+ // adjacent chars, only one side gets word highlights, creating a confusing
968
+ // visual ("off by 1" perception). Skip word-level in that case.
969
+ const wdBalanced = wd && wd.oldRanges.length > 0 && wd.newRanges.length > 0;
970
+ if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
999
971
  const delBody = injectBg(dels[0].hl, wd.oldRanges, BG_DEL, BG_DEL_W);
1000
972
  const addBody = injectBg(adds[0].hl, wd.newRanges, BG_ADD, BG_ADD_W);
1001
973
  emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, delBody, BG_DEL);
1002
974
  emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, addBody, BG_ADD);
1003
975
  continue;
1004
976
  }
1005
- if (isPaired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
977
+ if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
1006
978
  const pwd = plainWordDiff(dels[0].l.content, adds[0].l.content);
1007
979
  emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, `${BG_DEL}${pwd.old}`, BG_DEL);
1008
980
  emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, `${BG_ADD}${pwd.new}`, BG_ADD);
@@ -1166,9 +1138,9 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
1166
1138
  // ---------------------------------------------------------------------------
1167
1139
  // Extension
1168
1140
  // ---------------------------------------------------------------------------
1169
- exports.__testing = {
1141
+ export const __testing = {
1170
1142
  normalizeShikiContrast,
1171
- parseDiff: diff_js_1.parseDiff,
1143
+ parseDiff,
1172
1144
  renderSplit,
1173
1145
  renderUnified,
1174
1146
  };
@@ -1193,9 +1165,9 @@ function normalizeOptionalPositiveInteger(value, name) {
1193
1165
  throw new Error(`${name} must be a positive integer`);
1194
1166
  return number;
1195
1167
  }
1196
- async function diffRendererExtension(pi) {
1168
+ export default async function diffRendererExtension(pi) {
1197
1169
  // Apply diff theme palette from settings/presets before rendering
1198
- (0, hunk_preview_js_1.applyDiffPalette)();
1170
+ applySharedDiffPalette();
1199
1171
  let createWriteTool, createEditTool, getMarkdownTheme, TextComponent, MarkdownComponent;
1200
1172
  try {
1201
1173
  const sdk = await import("@earendil-works/pi-coding-agent");
@@ -1215,8 +1187,7 @@ async function diffRendererExtension(pi) {
1215
1187
  const cwd = process.cwd();
1216
1188
  const home = process.env.HOME ?? "";
1217
1189
  const sp = (p) => shortPath(cwd, home, p);
1218
- const reviewComments = [];
1219
- (0, command_js_1.registerReviewDiffCommand)(pi, cwd);
1190
+ registerReviewDiffCommand(pi, cwd);
1220
1191
  pi.registerTool({
1221
1192
  name: "review_git_diff",
1222
1193
  label: "Review Git Diff",
@@ -1271,18 +1242,18 @@ Examples:
1271
1242
  async execute(_tid, params = {}) {
1272
1243
  try {
1273
1244
  const safeParams = params ?? {};
1274
- const diff = await (0, git_js_1.readGitDiff)(cwd, reviewGitDiffMode(safeParams));
1245
+ const diff = await readGitDiff(cwd, reviewGitDiffMode(safeParams));
1275
1246
  const maxLinesPerHunk = reviewGitDiffMaxLines(safeParams);
1276
1247
  const markdown = safeParams.includeRawDiff || safeParams.raw
1277
- ? (0, export_js_1.formatReviewMarkdown)(diff, { includeRawDiff: true, maxLinesPerHunk })
1278
- : (0, interactive_js_1.formatInteractiveReviewPanel)(diff, reviewComments, {
1248
+ ? formatReviewMarkdown(diff, { includeRawDiff: true, maxLinesPerHunk })
1249
+ : formatInteractiveReviewPanel(diff, [], {
1279
1250
  file: safeParams.file,
1280
1251
  hunkId: safeParams.hunkId,
1281
1252
  maxFiles: normalizeOptionalPositiveInteger(safeParams.maxFiles, "maxFiles"),
1282
1253
  maxHunks: normalizeOptionalPositiveInteger(safeParams.maxHunks, "maxHunks"),
1283
1254
  maxLinesPerHunk,
1284
1255
  });
1285
- const counts = (0, git_js_1.countReviewDiffLines)(diff);
1256
+ const counts = countReviewDiffLines(diff);
1286
1257
  return {
1287
1258
  content: [{ type: "text", text: markdown }],
1288
1259
  details: {
@@ -1292,7 +1263,7 @@ Examples:
1292
1263
  fileCount: diff.files.length,
1293
1264
  insertions: counts.insertions,
1294
1265
  deletions: counts.deletions,
1295
- commentCount: reviewComments.length,
1266
+ commentCount: 0,
1296
1267
  focusedFile: safeParams.file,
1297
1268
  focusedHunk: safeParams.hunkId,
1298
1269
  },
@@ -1333,113 +1304,6 @@ Examples:
1333
1304
  return text;
1334
1305
  },
1335
1306
  });
1336
- pi.registerTool({
1337
- name: "review_git_comment",
1338
- label: "Draft Review Comment",
1339
- description: "Draft an inline code-review comment for the current interactive Git review. This stores comments in memory for the current Pi session and does not modify files or submit anything externally.",
1340
- promptSnippet: "Draft an inline comment for the current Git review session.",
1341
- parameters: {
1342
- type: "object",
1343
- properties: {
1344
- file: { type: "string", description: "Changed file path to comment on." },
1345
- line: { type: "number", description: "Optional old/new line number to comment on." },
1346
- hunkId: { type: "string", description: "Optional hunk id from review_git_diff output." },
1347
- body: { type: "string", description: "Comment body." },
1348
- },
1349
- required: ["file", "body"],
1350
- additionalProperties: false,
1351
- },
1352
- async execute(_tid, params) {
1353
- const file = typeof params?.file === "string" ? params.file.trim() : "";
1354
- const body = typeof params?.body === "string" ? params.body.trim() : "";
1355
- if (!file)
1356
- return {
1357
- content: [{ type: "text", text: "Error: file is required" }],
1358
- details: { error: "file required" },
1359
- };
1360
- if (!body)
1361
- return {
1362
- content: [{ type: "text", text: "Error: body is required" }],
1363
- details: { error: "body required" },
1364
- };
1365
- const comment = (0, interactive_js_1.createReviewComment)({
1366
- comments: reviewComments,
1367
- file,
1368
- body,
1369
- line: normalizeOptionalPositiveInteger(params.line, "line"),
1370
- hunkId: typeof params.hunkId === "string" && params.hunkId.trim() ? params.hunkId.trim() : undefined,
1371
- });
1372
- reviewComments.push(comment);
1373
- return {
1374
- content: [
1375
- {
1376
- type: "text",
1377
- text: `Drafted ${comment.id} on ${comment.file}${comment.line ? `:${comment.line}` : ""}\n\n${comment.body}`,
1378
- },
1379
- ],
1380
- details: { _type: "reviewGitComment", comment, commentCount: reviewComments.length },
1381
- };
1382
- },
1383
- renderCall(args, theme, ctx) {
1384
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1385
- text.setText(`${theme.fg("toolTitle", theme.bold("review_git_comment"))} ${theme.fg("accent", args?.file ?? "")}`);
1386
- return text;
1387
- },
1388
- renderResult(result, _opt, theme, ctx) {
1389
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1390
- if (ctx.isError || result.details?.error) {
1391
- text.setText(`\n${theme.fg("error", result.details?.error ?? "review_git_comment failed")}`);
1392
- return text;
1393
- }
1394
- const comment = result.details?.comment;
1395
- text.setText(` ${theme.fg("success", `✓ drafted ${comment?.id ?? "comment"}`)}${theme.fg("muted", ` (${result.details?.commentCount ?? 0} total)`)}`);
1396
- return text;
1397
- },
1398
- });
1399
- pi.registerTool({
1400
- name: "review_git_comments",
1401
- label: "Review Comments",
1402
- description: "List or clear drafted interactive Git review comments for the current Pi session.",
1403
- promptSnippet: "List or clear drafted review comments.",
1404
- parameters: {
1405
- type: "object",
1406
- properties: {
1407
- clear: { type: "boolean", description: "Clear all drafted comments when true." },
1408
- },
1409
- additionalProperties: false,
1410
- },
1411
- async execute(_tid, params = {}) {
1412
- if (params?.clear) {
1413
- const cleared = reviewComments.length;
1414
- reviewComments.length = 0;
1415
- return {
1416
- content: [{ type: "text", text: `Cleared ${cleared} drafted review comments.` }],
1417
- details: { _type: "reviewGitComments", cleared, commentCount: 0 },
1418
- };
1419
- }
1420
- const markdown = (0, interactive_js_1.formatReviewComments)(reviewComments);
1421
- return {
1422
- content: [{ type: "text", text: markdown }],
1423
- details: { _type: "reviewGitComments", markdown, commentCount: reviewComments.length },
1424
- };
1425
- },
1426
- renderCall(_args, theme, ctx) {
1427
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1428
- text.setText(theme.fg("toolTitle", theme.bold("review_git_comments")));
1429
- return text;
1430
- },
1431
- renderResult(result, _opt, theme, ctx) {
1432
- if (MarkdownComponent && getMarkdownTheme && typeof result.details?.markdown === "string") {
1433
- return new MarkdownComponent(result.details.markdown, 0, 0, getMarkdownTheme());
1434
- }
1435
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1436
- const cleared = typeof result.details?.cleared === "number"
1437
- ? `cleared ${result.details.cleared}`
1438
- : `${result.details?.commentCount ?? 0} drafted`;
1439
- text.setText(` ${theme.fg("muted", cleared)} review comments`);
1440
- return text;
1441
- },
1442
- });
1443
1307
  // =======================================================================
1444
1308
  // write
1445
1309
  // =======================================================================
@@ -1451,8 +1315,8 @@ Examples:
1451
1315
  const fp = params.path ?? params.file_path ?? "";
1452
1316
  let old = null;
1453
1317
  try {
1454
- if (fp && (0, node_fs_1.existsSync)(fp))
1455
- old = (0, node_fs_1.readFileSync)(fp, "utf-8");
1318
+ if (fp && existsSync(fp))
1319
+ old = readFileSync(fp, "utf-8");
1456
1320
  }
1457
1321
  catch {
1458
1322
  old = null;
@@ -1461,8 +1325,8 @@ Examples:
1461
1325
  const content = params.content ?? "";
1462
1326
  // Store in details — the only custom field TUI preserves in renderResult
1463
1327
  if (old !== null && old !== content) {
1464
- const diff = (0, diff_js_1.parseDiff)(old, content);
1465
- const lg = (0, hunk_preview_js_1.lang)(fp);
1328
+ const diff = parseDiff(old, content);
1329
+ const lg = detectDiffLanguage(fp);
1466
1330
  result.details = { _type: "diff", summary: summarize(diff.added, diff.removed), diff, language: lg };
1467
1331
  }
1468
1332
  else if (old === null) {
@@ -1476,7 +1340,7 @@ Examples:
1476
1340
  },
1477
1341
  renderCall(args, theme, ctx) {
1478
1342
  const fp = args?.path ?? args?.file_path ?? "";
1479
- const isNew = !fp || !(0, node_fs_1.existsSync)(fp);
1343
+ const isNew = !fp || !existsSync(fp);
1480
1344
  const label = isNew ? "create" : "write";
1481
1345
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1482
1346
  const hdr = `${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("accent", sp(fp))}`;
@@ -1488,11 +1352,11 @@ Examples:
1488
1352
  }
1489
1353
  // New file preview with Shiki
1490
1354
  if (args?.content && ctx.argsComplete && isNew) {
1491
- const previewKey = `create:${(0, hunk_preview_js_1.themeCacheKey)(theme)}:${fp}:${String(args.content).length}`;
1355
+ const previewKey = `create:${sharedThemeCacheKey(theme)}:${fp}:${String(args.content).length}`;
1492
1356
  if (ctx.state._previewKey !== previewKey) {
1493
1357
  ctx.state._previewKey = previewKey;
1494
1358
  ctx.state._previewText = hdr;
1495
- const lg = (0, hunk_preview_js_1.lang)(fp);
1359
+ const lg = detectDiffLanguage(fp);
1496
1360
  hlBlock(args.content, lg)
1497
1361
  .then((lines) => {
1498
1362
  if (ctx.state._previewKey !== previewKey)
@@ -1527,12 +1391,12 @@ Examples:
1527
1391
  const d = result.details;
1528
1392
  if (d?._type === "diff") {
1529
1393
  const w = termW();
1530
- const key = `wd:${(0, hunk_preview_js_1.themeCacheKey)(theme)}:${w}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}`;
1394
+ const key = `wd:${sharedThemeCacheKey(theme)}:${w}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}`;
1531
1395
  if (ctx.state._wdk !== key) {
1532
1396
  ctx.state._wdk = key;
1533
1397
  ctx.state._wdt = ` ${d.summary}\n${theme.fg("muted", " rendering diff…")}`;
1534
- const dc = (0, hunk_preview_js_1.resolveDiffColors)(theme);
1535
- (0, hunk_preview_js_1.renderSplit)(d.diff, d.language, MAX_RENDER_LINES, dc, w)
1398
+ const dc = resolveSharedDiffColors(theme);
1399
+ renderSharedSplit(d.diff, d.language, MAX_RENDER_LINES, dc, w)
1536
1400
  .then((rendered) => {
1537
1401
  if (ctx.state._wdk !== key)
1538
1402
  return;
@@ -1555,11 +1419,11 @@ Examples:
1555
1419
  }
1556
1420
  if (d?._type === "new") {
1557
1421
  const { lines: lineCount, content: rawContent, filePath: fp } = d;
1558
- const pk = `nf:${(0, hunk_preview_js_1.themeCacheKey)(theme)}:${fp}:${lineCount}`;
1422
+ const pk = `nf:${sharedThemeCacheKey(theme)}:${fp}:${lineCount}`;
1559
1423
  if (ctx.state._nfk !== pk) {
1560
1424
  ctx.state._nfk = pk;
1561
1425
  ctx.state._nft = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`;
1562
- const lg = (0, hunk_preview_js_1.lang)(fp);
1426
+ const lg = detectDiffLanguage(fp);
1563
1427
  if (rawContent) {
1564
1428
  hlBlock(rawContent, lg)
1565
1429
  .then((hlLines) => {
@@ -1602,7 +1466,7 @@ Examples:
1602
1466
  return oldText && oldText !== newText ? [{ oldText, newText }] : [];
1603
1467
  }
1604
1468
  function summarizeEditOperations(operations) {
1605
- const diffs = operations.map((edit) => (0, diff_js_1.parseDiff)(edit.oldText, edit.newText));
1469
+ const diffs = operations.map((edit) => parseDiff(edit.oldText, edit.newText));
1606
1470
  const totalAdded = diffs.reduce((sum, diff) => sum + diff.added, 0);
1607
1471
  const totalRemoved = diffs.reduce((sum, diff) => sum + diff.removed, 0);
1608
1472
  return {
@@ -1625,8 +1489,8 @@ Examples:
1625
1489
  if (operations.length === 1) {
1626
1490
  let editLine = 0;
1627
1491
  try {
1628
- if (fp && (0, node_fs_1.existsSync)(fp)) {
1629
- const f = (0, node_fs_1.readFileSync)(fp, "utf-8");
1492
+ if (fp && existsSync(fp)) {
1493
+ const f = readFileSync(fp, "utf-8");
1630
1494
  const idx = f.indexOf(operations[0].newText);
1631
1495
  if (idx >= 0)
1632
1496
  editLine = f.slice(0, idx).split("\n").length;
@@ -1655,15 +1519,15 @@ Examples:
1655
1519
  text.setText(hdr);
1656
1520
  return text;
1657
1521
  }
1658
- const pk = JSON.stringify({ fp, operations, theme: (0, hunk_preview_js_1.themeCacheKey)(theme), w: termW() });
1522
+ const pk = JSON.stringify({ fp, operations, theme: sharedThemeCacheKey(theme), w: termW() });
1659
1523
  if (ctx.state._pk !== pk) {
1660
1524
  ctx.state._pk = pk;
1661
1525
  ctx.state._pt = `${hdr} ${theme.fg("muted", "(rendering…)")}`;
1662
- const lg = (0, hunk_preview_js_1.lang)(fp);
1663
- const dc = (0, hunk_preview_js_1.resolveDiffColors)(theme);
1526
+ const lg = detectDiffLanguage(fp);
1527
+ const dc = resolveSharedDiffColors(theme);
1664
1528
  if (operations.length === 1) {
1665
- const diff = (0, diff_js_1.parseDiff)(operations[0].oldText, operations[0].newText);
1666
- (0, hunk_preview_js_1.renderSplit)(diff, lg, MAX_PREVIEW_LINES, dc, termW())
1529
+ const diff = parseDiff(operations[0].oldText, operations[0].newText);
1530
+ renderSharedSplit(diff, lg, MAX_PREVIEW_LINES, dc, termW())
1667
1531
  .then((rendered) => {
1668
1532
  if (ctx.state._pk !== pk)
1669
1533
  return;
@@ -1681,7 +1545,7 @@ Examples:
1681
1545
  const { diffs, summary } = summarizeEditOperations(operations);
1682
1546
  const maxShown = Math.min(operations.length, 3);
1683
1547
  const previewLines = Math.max(8, Math.floor(MAX_PREVIEW_LINES / maxShown));
1684
- Promise.all(diffs.slice(0, maxShown).map((diff, index) => (0, hunk_preview_js_1.renderSplit)(diff, lg, previewLines, dc, termW())
1548
+ Promise.all(diffs.slice(0, maxShown).map((diff, index) => renderSharedSplit(diff, lg, previewLines, dc, termW())
1685
1549
  .then((rendered) => `Edit ${index + 1}/${operations.length}\n${rendered}`)
1686
1550
  .catch(() => `Edit ${index + 1}/${operations.length} ${summarize(diff.added, diff.removed)}`)))
1687
1551
  .then((sections) => {