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 +1 -0
- package/package.json +1 -1
- package/src/review/component.ts +201 -24
- package/src/review/navigation.ts +7 -0
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
package/src/review/component.ts
CHANGED
|
@@ -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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
1606
|
-
|
|
1607
|
-
|
|
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
|
|
1742
|
-
|
|
1743
|
-
|
|
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) {
|
package/src/review/navigation.ts
CHANGED
|
@@ -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,
|