pi-diff-review 0.1.23 → 0.1.25

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 CHANGED
@@ -88,6 +88,7 @@ Open one or more files or folders with `/view`:
88
88
  - `ctrl-u` / `ctrl-d` to move up/down by half a page
89
89
  - `s` toggles inline comments/explanations
90
90
  - `v` toggles the diff between unified and side-by-side split rendering
91
+ - `w` toggles line wrap for long diff lines
91
92
  - `?` toggles an AI-generated explanation for the current hunk
92
93
  - `/` searches visible diff text, highlights matches, and `n/N` moves between them while a search is active
93
94
  - `J/K` to extend a highlighted selection into a comment range
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-diff-review",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Local diff review TUI extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -55,8 +55,8 @@ type InlineBoxRow = {
55
55
 
56
56
  type AnnotatedDiffRow =
57
57
  | { kind: "file-header"; file: ReviewFileSection }
58
- | { kind: "diff"; lineIndex: number }
59
- | { kind: "split"; splitRowIndex: number }
58
+ | { kind: "diff"; lineIndex: number; segmentIndex: number }
59
+ | { kind: "split"; splitRowIndex: number; segmentIndex: number }
60
60
  | InlineBoxRow;
61
61
 
62
62
  const HELP_COMMANDS = [
@@ -77,6 +77,7 @@ const HELP_COMMANDS = [
77
77
  ["t", "toggle the file sidebar"],
78
78
  ["s", "toggle inline comments and explanations"],
79
79
  ["v", "toggle unified or split rendering"],
80
+ ["w", "toggle line wrap for long diff lines"],
80
81
  ["?", "toggle AI explanation for current hunk"],
81
82
  ["a", "ask a question about the current hunk"],
82
83
  ["Enter", "submit comments, save edits, or jump to search result"],
@@ -90,7 +91,7 @@ export class ReviewComponent {
90
91
  private editingCommentKey?: string;
91
92
  private search: ReviewSearchState;
92
93
  private helpVisible = false;
93
- private fileSidebarVisible = false;
94
+ private fileSidebarVisible = true;
94
95
 
95
96
  private inlineAnnotationsVisible = true;
96
97
  private visibleExplanationKeys = new Set<string>();
@@ -114,6 +115,7 @@ export class ReviewComponent {
114
115
  private annotatedRowsEditMode = false;
115
116
  private annotatedRowsEditingCommentKey?: string;
116
117
  private annotatedRowsMode?: DiffRenderMode;
118
+ private annotatedRowsLineWrapEnabled = false;
117
119
  private annotatedRowsInlineAnnotationsVisible = true;
118
120
  private annotatedRowsVisibleExplanationCount = 0;
119
121
  private annotatedRowByLineIndex?: number[];
@@ -258,6 +260,10 @@ export class ReviewComponent {
258
260
  return this.navigation.diffRenderMode;
259
261
  }
260
262
 
263
+ private get lineWrapEnabled(): boolean {
264
+ return this.navigation.lineWrapEnabled;
265
+ }
266
+
261
267
  handleInput(data: string): void {
262
268
  if (this.helpVisible) {
263
269
  if (data === "h" || matchesKey(data, "escape")) {
@@ -330,6 +336,10 @@ export class ReviewComponent {
330
336
  this.toggleDiffRenderMode();
331
337
  return;
332
338
  }
339
+ if (data === "w") {
340
+ this.toggleLineWrap();
341
+ return;
342
+ }
333
343
  if (data === "?") {
334
344
  this.toggleExplanationPane();
335
345
  return;
@@ -626,6 +636,7 @@ export class ReviewComponent {
626
636
  this.renderDiffLine(
627
637
  line,
628
638
  index,
639
+ annotated.segmentIndex,
629
640
  width,
630
641
  index === this.selected,
631
642
  this.getSelectionBounds(),
@@ -635,7 +646,13 @@ export class ReviewComponent {
635
646
  }
636
647
 
637
648
  if (annotated.kind === "split") {
638
- output.push(this.renderSplitDiffRowAt(annotated.splitRowIndex, width));
649
+ output.push(
650
+ this.renderSplitDiffRowAt(
651
+ annotated.splitRowIndex,
652
+ annotated.segmentIndex,
653
+ width,
654
+ ),
655
+ );
639
656
  continue;
640
657
  }
641
658
 
@@ -653,6 +670,7 @@ export class ReviewComponent {
653
670
  this.annotatedRowsEditMode === this.editMode &&
654
671
  this.annotatedRowsEditingCommentKey === this.editingCommentKey &&
655
672
  this.annotatedRowsMode === this.diffRenderMode &&
673
+ this.annotatedRowsLineWrapEnabled === this.lineWrapEnabled &&
656
674
  this.annotatedRowsInlineAnnotationsVisible ===
657
675
  this.inlineAnnotationsVisible &&
658
676
  this.annotatedRowsVisibleExplanationCount ===
@@ -682,6 +700,7 @@ export class ReviewComponent {
682
700
  this.annotatedRowsEditMode = this.editMode;
683
701
  this.annotatedRowsEditingCommentKey = this.editingCommentKey;
684
702
  this.annotatedRowsMode = this.diffRenderMode;
703
+ this.annotatedRowsLineWrapEnabled = this.lineWrapEnabled;
685
704
  this.annotatedRowsInlineAnnotationsVisible = this.inlineAnnotationsVisible;
686
705
  this.annotatedRowsVisibleExplanationCount =
687
706
  this.visibleExplanationKeys.size;
@@ -702,7 +721,18 @@ export class ReviewComponent {
702
721
  index++
703
722
  ) {
704
723
  rowByLineIndex[index] = rows.length;
705
- rows.push({ kind: "diff", lineIndex: index });
724
+ const segmentCount = this.getDiffRowSegmentCount(
725
+ this.lines[index]!,
726
+ index,
727
+ width,
728
+ );
729
+ for (
730
+ let segmentIndex = 0;
731
+ segmentIndex < segmentCount;
732
+ segmentIndex++
733
+ ) {
734
+ rows.push({ kind: "diff", lineIndex: index, segmentIndex });
735
+ }
706
736
 
707
737
  if (!this.inlineAnnotationsVisible) continue;
708
738
 
@@ -736,7 +766,14 @@ export class ReviewComponent {
736
766
  rowByLineIndex[lineIndex] = rows.length;
737
767
  }
738
768
 
739
- rows.push({ kind: "split", splitRowIndex });
769
+ const segmentCount = this.getSplitRowSegmentCount(splitRow, width);
770
+ for (
771
+ let segmentIndex = 0;
772
+ segmentIndex < segmentCount;
773
+ segmentIndex++
774
+ ) {
775
+ rows.push({ kind: "split", splitRowIndex, segmentIndex });
776
+ }
740
777
  if (!this.inlineAnnotationsVisible) continue;
741
778
 
742
779
  for (const lineIndex of lineIndexes) {
@@ -1183,7 +1220,11 @@ export class ReviewComponent {
1183
1220
  );
1184
1221
  }
1185
1222
 
1186
- private renderSplitDiffRowAt(splitRowIndex: number, width: number): string {
1223
+ private renderSplitDiffRowAt(
1224
+ splitRowIndex: number,
1225
+ segmentIndex: number,
1226
+ width: number,
1227
+ ): string {
1187
1228
  const splitRow = this.getSplitDiffRows()[splitRowIndex];
1188
1229
  if (!splitRow) return " ".repeat(width);
1189
1230
 
@@ -1191,6 +1232,7 @@ export class ReviewComponent {
1191
1232
  return this.renderDiffLine(
1192
1233
  splitRow.cell.line,
1193
1234
  splitRow.cell.index,
1235
+ segmentIndex,
1194
1236
  width,
1195
1237
  splitRow.cell.index === this.selected,
1196
1238
  this.getSelectionBounds(),
@@ -1201,10 +1243,15 @@ export class ReviewComponent {
1201
1243
  const leftWidth = Math.max(10, Math.floor((width - separatorWidth) / 2));
1202
1244
  const rightWidth = Math.max(10, width - leftWidth - separatorWidth);
1203
1245
  const left = splitRow.left
1204
- ? this.renderSplitDiffCell(splitRow.left, leftWidth, "left")
1246
+ ? this.renderSplitDiffCell(splitRow.left, leftWidth, "left", segmentIndex)
1205
1247
  : " ".repeat(leftWidth);
1206
1248
  const right = splitRow.right
1207
- ? this.renderSplitDiffCell(splitRow.right, rightWidth, "right")
1249
+ ? this.renderSplitDiffCell(
1250
+ splitRow.right,
1251
+ rightWidth,
1252
+ "right",
1253
+ segmentIndex,
1254
+ )
1208
1255
  : " ".repeat(rightWidth);
1209
1256
  return truncateToWidth(
1210
1257
  `${padToWidth(left, leftWidth)}${this.theme.fg("borderMuted", " │ ")}${padToWidth(right, rightWidth)}`,
@@ -1212,6 +1259,137 @@ export class ReviewComponent {
1212
1259
  );
1213
1260
  }
1214
1261
 
1262
+ private getDiffRowSegmentCount(
1263
+ line: ReviewLine,
1264
+ index: number,
1265
+ width: number,
1266
+ ): number {
1267
+ return this.getDiffRowSegments(line, index, width).length;
1268
+ }
1269
+
1270
+ private getSplitRowSegmentCount(row: SplitDiffRow, width: number): number {
1271
+ if (row.kind === "full") {
1272
+ return this.getDiffRowSegmentCount(row.cell.line, row.cell.index, width);
1273
+ }
1274
+
1275
+ const separatorWidth = 3;
1276
+ const leftWidth = Math.max(10, Math.floor((width - separatorWidth) / 2));
1277
+ const rightWidth = Math.max(10, width - leftWidth - separatorWidth);
1278
+ return Math.max(
1279
+ row.left
1280
+ ? this.getSplitDiffCellSegments(row.left, leftWidth, "left").length
1281
+ : 1,
1282
+ row.right
1283
+ ? this.getSplitDiffCellSegments(row.right, rightWidth, "right").length
1284
+ : 1,
1285
+ );
1286
+ }
1287
+
1288
+ private getDiffRowSegments(
1289
+ line: ReviewLine,
1290
+ index: number,
1291
+ width: number,
1292
+ ): string[] {
1293
+ const hasComment = this.getCommentKeysForLine(index).length > 0;
1294
+ const commentMark = hasComment ? this.theme.fg("borderAccent", "│") : " ";
1295
+ const numbers = `${lineNumberCell(line.oldLineNumber)} ${lineNumberCell(line.newLineNumber)}`;
1296
+ const prefix = this.styleDiffPrefix(line, `${commentMark} ${numbers} `);
1297
+ const continuationPrefix = this.styleDiffPrefix(
1298
+ line,
1299
+ `${commentMark} ${lineNumberCell()} ${lineNumberCell()} `,
1300
+ );
1301
+ return this.wrapDiffContentSegments(
1302
+ this.getRenderedDiffContent(line, index),
1303
+ prefix,
1304
+ continuationPrefix,
1305
+ width,
1306
+ );
1307
+ }
1308
+
1309
+ private getSplitDiffCellSegments(
1310
+ cell: SplitDiffCell,
1311
+ width: number,
1312
+ side: "left" | "right",
1313
+ ): string[] {
1314
+ const { line, index } = cell;
1315
+ const hasComment = this.getCommentKeysForLine(index).length > 0;
1316
+ const commentMark = hasComment ? this.theme.fg("borderAccent", "│") : " ";
1317
+ const lineNumber =
1318
+ side === "left" ? line.oldLineNumber : line.newLineNumber;
1319
+ const prefix = this.styleDiffPrefix(
1320
+ line,
1321
+ `${commentMark} ${lineNumberCell(lineNumber)} `,
1322
+ );
1323
+ const continuationPrefix = this.styleDiffPrefix(
1324
+ line,
1325
+ `${commentMark} ${lineNumberCell()} `,
1326
+ );
1327
+ return this.wrapDiffContentSegments(
1328
+ this.getRenderedDiffContent(line, index),
1329
+ prefix,
1330
+ continuationPrefix,
1331
+ width,
1332
+ );
1333
+ }
1334
+
1335
+ private getRenderedDiffContent(line: ReviewLine, index: number): string {
1336
+ switch (line.kind) {
1337
+ case "add":
1338
+ case "remove":
1339
+ case "context":
1340
+ return this.getHighlightedDisplayText(line, index);
1341
+ case "hunk":
1342
+ return this.theme.fg("accent", this.getDisplayText(line));
1343
+ default:
1344
+ return this.theme.fg("muted", this.getDisplayText(line));
1345
+ }
1346
+ }
1347
+
1348
+ private styleDiffPrefix(line: ReviewLine, text: string): string {
1349
+ switch (line.kind) {
1350
+ case "add":
1351
+ return this.theme.fg("toolDiffAdded", text);
1352
+ case "remove":
1353
+ return this.theme.fg("toolDiffRemoved", text);
1354
+ case "context":
1355
+ return this.theme.fg("toolDiffContext", text);
1356
+ case "hunk":
1357
+ return this.theme.fg("accent", text);
1358
+ default:
1359
+ return this.theme.fg("muted", text);
1360
+ }
1361
+ }
1362
+
1363
+ private wrapDiffContentSegments(
1364
+ content: string,
1365
+ prefix: string,
1366
+ continuationPrefix: string,
1367
+ width: number,
1368
+ ): string[] {
1369
+ const firstLineWidth = Math.max(1, width - visibleWidth(prefix));
1370
+ const continuationWidth = Math.max(
1371
+ 1,
1372
+ width - visibleWidth(continuationPrefix),
1373
+ );
1374
+
1375
+ if (!this.lineWrapEnabled) {
1376
+ return [`${prefix}${truncateToWidth(content, firstLineWidth)}`];
1377
+ }
1378
+
1379
+ const firstSegments = wrapTextWithAnsi(content, firstLineWidth);
1380
+ if (firstSegments.length === 0) return [prefix];
1381
+ if (firstSegments.length === 1) return [`${prefix}${firstSegments[0]}`];
1382
+
1383
+ const [firstSegment, ...remaining] = firstSegments;
1384
+ const wrapped = [`${prefix}${firstSegment}`];
1385
+ for (const segment of remaining) {
1386
+ for (const continuation of wrapTextWithAnsi(segment, continuationWidth)) {
1387
+ wrapped.push(`${continuationPrefix}${continuation}`);
1388
+ }
1389
+ }
1390
+ return wrapped;
1391
+ }
1392
+
1215
1393
  private getContentHeight(): number {
1216
1394
  const terminalRows = this.tui.terminal?.rows ?? 24;
1217
1395
  const headerHeight = 3;
@@ -1326,6 +1504,12 @@ export class ReviewComponent {
1326
1504
  this.tui.requestRender(true);
1327
1505
  }
1328
1506
 
1507
+ private toggleLineWrap(): void {
1508
+ this.navigation.toggleLineWrap();
1509
+ this.invalidateAnnotatedRows();
1510
+ this.tui.requestRender(true);
1511
+ }
1512
+
1329
1513
  private toggleInlineAnnotations(): void {
1330
1514
  this.inlineAnnotationsVisible = !this.inlineAnnotationsVisible;
1331
1515
  this.invalidateAnnotatedRows();
@@ -1600,16 +1784,12 @@ export class ReviewComponent {
1600
1784
  cell: SplitDiffCell,
1601
1785
  width: number,
1602
1786
  side: "left" | "right",
1787
+ segmentIndex: number,
1603
1788
  ): string {
1604
1789
  const { line, index } = cell;
1605
- const hasComment = this.getCommentKeysForLine(index).length > 0;
1606
- const commentMark = hasComment ? this.theme.fg("borderAccent", "│") : " ";
1607
- const lineNumber =
1608
- side === "left" ? line.oldLineNumber : line.newLineNumber;
1609
- const prefix = `${commentMark} ${lineNumberCell(lineNumber)} `;
1610
- let styled = this.renderDiffRowContent(line, prefix, index);
1611
-
1612
- styled = truncateToWidth(styled, width);
1790
+ const styled =
1791
+ this.getSplitDiffCellSegments(cell, width, side)[segmentIndex] ??
1792
+ " ".repeat(width);
1613
1793
  const selection = this.getSelectionBounds();
1614
1794
  const inSelection =
1615
1795
  selection != null && index >= selection.start && index <= selection.end;
@@ -1734,17 +1914,14 @@ export class ReviewComponent {
1734
1914
  private renderDiffLine(
1735
1915
  line: ReviewLine,
1736
1916
  index: number,
1917
+ segmentIndex: number,
1737
1918
  width: number,
1738
1919
  selected: boolean,
1739
1920
  selection?: SelectionBounds,
1740
1921
  ): string {
1741
- const hasComment = this.getCommentKeysForLine(index).length > 0;
1742
- const commentMark = hasComment ? this.theme.fg("borderAccent", "│") : " ";
1743
- const numbers = `${lineNumberCell(line.oldLineNumber)} ${lineNumberCell(line.newLineNumber)}`;
1744
- const prefix = `${commentMark} ${numbers} `;
1745
- let styled = this.renderDiffRowContent(line, prefix, index);
1746
-
1747
- styled = truncateToWidth(styled, width);
1922
+ const styled =
1923
+ this.getDiffRowSegments(line, index, width)[segmentIndex] ??
1924
+ " ".repeat(width);
1748
1925
  const inSelection =
1749
1926
  selection != null && index >= selection.start && index <= selection.end;
1750
1927
  if (selected || inSelection) {
@@ -10,6 +10,7 @@ export class ReviewNavigationState {
10
10
  scrollTop = 0;
11
11
  selectionAnchor?: number;
12
12
  diffRenderMode: DiffRenderMode = "unified";
13
+ lineWrapEnabled = false;
13
14
 
14
15
  constructor(
15
16
  private readonly lineCount: number,
@@ -84,6 +85,12 @@ export class ReviewNavigationState {
84
85
  return this.diffRenderMode;
85
86
  }
86
87
 
88
+ toggleLineWrap(): boolean {
89
+ this.lineWrapEnabled = !this.lineWrapEnabled;
90
+ this.scrollTop = 0;
91
+ return this.lineWrapEnabled;
92
+ }
93
+
87
94
  ensureScroll(
88
95
  viewportHeight: number,
89
96
  selectedDisplayRow: number,