pi-diff-review 0.1.13 → 0.1.14
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 +3 -3
- package/package.json +1 -1
- package/src/prompt.ts +1 -1
- package/src/review-component.ts +469 -171
- package/src/types.ts +0 -2
- package/src/review-panes.ts +0 -286
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# pi-diff-review
|
|
4
4
|
|
|
5
|
-
Embedded code reviews
|
|
5
|
+
Embedded code reviews and AI summaries directly within [pi](https://pi.dev/).
|
|
6
6
|
|
|
7
|
-
<img width="1986" height="1556" alt="pi-diff-review-screenshot
|
|
7
|
+
<img width="1986" height="1556" alt="pi-diff-review-screenshot" src="https://github.com/user-attachments/assets/5ddd7226-28d4-4617-8c5c-35b4b6af68dc" />
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -65,7 +65,7 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
|
|
|
65
65
|
- `j/k` or arrow keys to move
|
|
66
66
|
- `g/G` to jump to the top or bottom of the diff
|
|
67
67
|
- `ctrl-u` / `ctrl-d` to move up/down by half a page
|
|
68
|
-
- `t` toggles
|
|
68
|
+
- `t` toggles inline comments/explanations
|
|
69
69
|
- `v` toggles the diff between unified and side-by-side split rendering
|
|
70
70
|
- `?` toggles an AI-generated explanation for the current hunk
|
|
71
71
|
- `J/K` to extend a highlighted selection into a comment range
|
package/package.json
CHANGED
package/src/prompt.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function formatLocation(line: {
|
|
|
17
17
|
return file;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function formatCommentLocation(comment: ReviewComment): string {
|
|
20
|
+
export function formatCommentLocation(comment: ReviewComment): string {
|
|
21
21
|
if (comment.global) return "Overall diff";
|
|
22
22
|
|
|
23
23
|
const start = formatLocation({
|
package/src/review-component.ts
CHANGED
|
@@ -2,7 +2,13 @@ import {
|
|
|
2
2
|
getLanguageFromPath,
|
|
3
3
|
highlightCode,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Editor,
|
|
7
|
+
matchesKey,
|
|
8
|
+
truncateToWidth,
|
|
9
|
+
visibleWidth,
|
|
10
|
+
wrapTextWithAnsi,
|
|
11
|
+
} from "@earendil-works/pi-tui";
|
|
6
12
|
import type { DiffExplainer } from "./explain.ts";
|
|
7
13
|
import {
|
|
8
14
|
GLOBAL_COMMENT_KEY,
|
|
@@ -15,12 +21,8 @@ import {
|
|
|
15
21
|
ExplanationController,
|
|
16
22
|
getCurrentHunkScope,
|
|
17
23
|
} from "./explanation-controller.ts";
|
|
18
|
-
import { formatLocation } from "./prompt.ts";
|
|
24
|
+
import { formatCommentLocation, formatLocation } from "./prompt.ts";
|
|
19
25
|
import { ReviewNavigationState } from "./review-navigation.ts";
|
|
20
|
-
import {
|
|
21
|
-
renderCommentsPane as renderCommentsPaneContent,
|
|
22
|
-
renderExplanationPane as renderExplanationPaneContent,
|
|
23
|
-
} from "./review-panes.ts";
|
|
24
26
|
import { padToWidth, lineNumberCell } from "./render-utils.ts";
|
|
25
27
|
import { buildSplitDiffRows } from "./split-diff.ts";
|
|
26
28
|
import type {
|
|
@@ -30,28 +32,48 @@ import type {
|
|
|
30
32
|
ReviewResult,
|
|
31
33
|
ReviewTheme,
|
|
32
34
|
ReviewTui,
|
|
33
|
-
RightPaneMode,
|
|
34
35
|
SelectionBounds,
|
|
35
36
|
SplitDiffCell,
|
|
36
37
|
SplitDiffRow,
|
|
37
38
|
} from "./types.ts";
|
|
38
39
|
|
|
40
|
+
type InlineBoxPart = "top" | "body" | "bottom";
|
|
41
|
+
type InlineBoxRowKind = "comment" | "editor" | "explanation";
|
|
42
|
+
type InlineBoxRow = {
|
|
43
|
+
kind: InlineBoxRowKind;
|
|
44
|
+
text: string;
|
|
45
|
+
part: InlineBoxPart;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type AnnotatedDiffRow =
|
|
49
|
+
| { kind: "diff"; lineIndex: number }
|
|
50
|
+
| { kind: "split"; splitRowIndex: number }
|
|
51
|
+
| InlineBoxRow;
|
|
52
|
+
|
|
39
53
|
export class ReviewComponent {
|
|
40
54
|
private navigation: ReviewNavigationState;
|
|
41
55
|
private editMode = false;
|
|
42
56
|
private editingCommentKey?: string;
|
|
43
57
|
|
|
44
|
-
private
|
|
45
|
-
private
|
|
58
|
+
private inlineAnnotationsVisible = true;
|
|
59
|
+
private visibleExplanationKeys = new Set<string>();
|
|
46
60
|
private explanationController: ExplanationController;
|
|
47
61
|
private editor: Editor;
|
|
48
62
|
private splitRows?: SplitDiffRow[];
|
|
49
|
-
private splitRowByLineIndex?: number[];
|
|
50
63
|
private lineIndexById = new Map<string, number>();
|
|
51
64
|
private commentLineKeys = new Map<number, string[]>();
|
|
52
65
|
private commentsRevision = 0;
|
|
53
66
|
private commentLineKeysRevision = -1;
|
|
54
67
|
private highlightedLineCache = new Map<string, string>();
|
|
68
|
+
private annotatedRows?: AnnotatedDiffRow[];
|
|
69
|
+
private annotatedRowsWidth = 0;
|
|
70
|
+
private annotatedRowsRevision = -1;
|
|
71
|
+
private annotatedRowsEditMode = false;
|
|
72
|
+
private annotatedRowsEditingCommentKey?: string;
|
|
73
|
+
private annotatedRowsMode?: DiffRenderMode;
|
|
74
|
+
private annotatedRowsInlineAnnotationsVisible = true;
|
|
75
|
+
private annotatedRowsVisibleExplanationCount = 0;
|
|
76
|
+
private annotatedRowByLineIndex?: number[];
|
|
55
77
|
|
|
56
78
|
constructor(
|
|
57
79
|
private tui: ReviewTui,
|
|
@@ -120,6 +142,7 @@ export class ReviewComponent {
|
|
|
120
142
|
this.markCommentsChanged();
|
|
121
143
|
}
|
|
122
144
|
|
|
145
|
+
this.navigation.clearSelection();
|
|
123
146
|
this.exitEditMode();
|
|
124
147
|
};
|
|
125
148
|
}
|
|
@@ -151,6 +174,7 @@ export class ReviewComponent {
|
|
|
151
174
|
return;
|
|
152
175
|
}
|
|
153
176
|
this.editor.handleInput(data);
|
|
177
|
+
this.invalidateAnnotatedRows();
|
|
154
178
|
this.tui.requestRender();
|
|
155
179
|
return;
|
|
156
180
|
}
|
|
@@ -168,7 +192,7 @@ export class ReviewComponent {
|
|
|
168
192
|
return;
|
|
169
193
|
}
|
|
170
194
|
if (data === "t") {
|
|
171
|
-
this.
|
|
195
|
+
this.toggleInlineAnnotations();
|
|
172
196
|
return;
|
|
173
197
|
}
|
|
174
198
|
if (data === "v") {
|
|
@@ -250,23 +274,17 @@ export class ReviewComponent {
|
|
|
250
274
|
this.theme.fg(
|
|
251
275
|
"dim",
|
|
252
276
|
this.editMode
|
|
253
|
-
? `${this.lines.length} lines • ${this.comments.size} comments • editing comment • Enter save • Esc/Ctrl+C cancel`
|
|
277
|
+
? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • editing inline comment • Enter save • Esc/Ctrl+C cancel`
|
|
254
278
|
: this.hasSelection()
|
|
255
|
-
? `${this.lines.length} lines • ${this.comments.size} comments • J/K extend • Esc clear selection • c comment range • C overall comment • Enter submit`
|
|
256
|
-
: `${this.lines.length} lines • ${this.comments.size} comments • ${this.getPositionText(selectedLine)} • ${this.
|
|
279
|
+
? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • J/K extend • Esc clear selection • c comment range • C overall comment • Enter submit`
|
|
280
|
+
: `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • ${this.getPositionText(selectedLine)} • inline ${this.inlineAnnotationsVisible ? "shown" : "hidden"} • j/k move • g/G top/bottom • ctrl-u/d page • t annotations • v unified/split • ? explain • J/K extend • c comment • C overall • x delete • n/p hunk • Enter submit • q quit`,
|
|
257
281
|
),
|
|
258
282
|
width,
|
|
259
283
|
),
|
|
260
284
|
);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
} else {
|
|
265
|
-
this.ensureScroll(viewportHeight);
|
|
266
|
-
output.push(
|
|
267
|
-
...this.renderSideBySide(width, viewportHeight, selectedLine),
|
|
268
|
-
);
|
|
269
|
-
}
|
|
285
|
+
|
|
286
|
+
this.ensureScroll(viewportHeight, width);
|
|
287
|
+
output.push(...this.renderAnnotatedDiffRows(width, viewportHeight));
|
|
270
288
|
|
|
271
289
|
output.push(
|
|
272
290
|
truncateToWidth(
|
|
@@ -277,105 +295,415 @@ export class ReviewComponent {
|
|
|
277
295
|
return output;
|
|
278
296
|
}
|
|
279
297
|
|
|
280
|
-
private
|
|
281
|
-
width
|
|
282
|
-
height: number,
|
|
283
|
-
selectedLine?: ReviewLine,
|
|
284
|
-
): string[] {
|
|
285
|
-
const rightWidth = Math.max(28, Math.floor(width * 0.34));
|
|
286
|
-
const separatorWidth = 3;
|
|
287
|
-
const leftWidth = Math.max(30, width - rightWidth - separatorWidth);
|
|
288
|
-
const rightPane = this.renderRightPane(rightWidth, height, selectedLine);
|
|
298
|
+
private renderAnnotatedDiffRows(width: number, height: number): string[] {
|
|
299
|
+
const rows = this.getAnnotatedRows(width);
|
|
289
300
|
const output: string[] = [];
|
|
290
|
-
const diffPane = this.renderDiffRows(leftWidth, height);
|
|
291
301
|
|
|
292
302
|
for (let row = 0; row < height; row++) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
303
|
+
const annotated = rows[this.scrollTop + row];
|
|
304
|
+
if (!annotated) {
|
|
305
|
+
output.push(" ".repeat(width));
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (annotated.kind === "diff") {
|
|
310
|
+
const index = annotated.lineIndex;
|
|
311
|
+
const line = this.lines[index]!;
|
|
312
|
+
output.push(
|
|
313
|
+
this.renderDiffLine(
|
|
314
|
+
line,
|
|
315
|
+
index,
|
|
316
|
+
width,
|
|
317
|
+
index === this.selected,
|
|
318
|
+
this.getSelectionBounds(),
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (annotated.kind === "split") {
|
|
325
|
+
output.push(this.renderSplitDiffRowAt(annotated.splitRowIndex, width));
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
output.push(this.renderInlineAnnotationRow(annotated, width));
|
|
297
330
|
}
|
|
298
331
|
|
|
299
332
|
return output;
|
|
300
333
|
}
|
|
301
334
|
|
|
302
|
-
private
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
335
|
+
private getAnnotatedRows(width: number): AnnotatedDiffRow[] {
|
|
336
|
+
if (
|
|
337
|
+
this.annotatedRows &&
|
|
338
|
+
this.annotatedRowsWidth === width &&
|
|
339
|
+
this.annotatedRowsRevision === this.commentsRevision &&
|
|
340
|
+
this.annotatedRowsEditMode === this.editMode &&
|
|
341
|
+
this.annotatedRowsEditingCommentKey === this.editingCommentKey &&
|
|
342
|
+
this.annotatedRowsMode === this.diffRenderMode &&
|
|
343
|
+
this.annotatedRowsInlineAnnotationsVisible ===
|
|
344
|
+
this.inlineAnnotationsVisible &&
|
|
345
|
+
this.annotatedRowsVisibleExplanationCount ===
|
|
346
|
+
this.visibleExplanationKeys.size &&
|
|
347
|
+
this.visibleExplanationKeys.size === 0
|
|
348
|
+
) {
|
|
349
|
+
return this.annotatedRows;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const rows: AnnotatedDiffRow[] = [];
|
|
353
|
+
const rowByLineIndex: number[] = [];
|
|
354
|
+
|
|
355
|
+
this.pushGlobalAnnotationRows(rows, width);
|
|
356
|
+
|
|
357
|
+
if (this.diffRenderMode === "split") {
|
|
358
|
+
this.pushAnnotatedSplitRows(rows, rowByLineIndex, width);
|
|
359
|
+
} else {
|
|
360
|
+
this.pushAnnotatedUnifiedRows(rows, rowByLineIndex, width);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.annotatedRows = rows;
|
|
364
|
+
this.annotatedRowsWidth = width;
|
|
365
|
+
this.annotatedRowsRevision = this.commentsRevision;
|
|
366
|
+
this.annotatedRowsEditMode = this.editMode;
|
|
367
|
+
this.annotatedRowsEditingCommentKey = this.editingCommentKey;
|
|
368
|
+
this.annotatedRowsMode = this.diffRenderMode;
|
|
369
|
+
this.annotatedRowsInlineAnnotationsVisible = this.inlineAnnotationsVisible;
|
|
370
|
+
this.annotatedRowsVisibleExplanationCount =
|
|
371
|
+
this.visibleExplanationKeys.size;
|
|
372
|
+
this.annotatedRowByLineIndex = rowByLineIndex;
|
|
373
|
+
return rows;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private pushAnnotatedUnifiedRows(
|
|
377
|
+
rows: AnnotatedDiffRow[],
|
|
378
|
+
rowByLineIndex: number[],
|
|
379
|
+
width: number,
|
|
380
|
+
): void {
|
|
381
|
+
for (let index = 0; index < this.lines.length; index++) {
|
|
382
|
+
rowByLineIndex[index] = rows.length;
|
|
383
|
+
rows.push({ kind: "diff", lineIndex: index });
|
|
384
|
+
|
|
385
|
+
if (!this.inlineAnnotationsVisible) continue;
|
|
386
|
+
|
|
387
|
+
this.pushInlineCommentRows(rows, index, width);
|
|
388
|
+
this.pushInlineEditorRows(rows, index, width);
|
|
389
|
+
this.pushInlineExplanationRows(rows, index, width);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private pushAnnotatedSplitRows(
|
|
394
|
+
rows: AnnotatedDiffRow[],
|
|
395
|
+
rowByLineIndex: number[],
|
|
396
|
+
width: number,
|
|
397
|
+
): void {
|
|
398
|
+
const splitRows = this.getSplitDiffRows();
|
|
399
|
+
for (
|
|
400
|
+
let splitRowIndex = 0;
|
|
401
|
+
splitRowIndex < splitRows.length;
|
|
402
|
+
splitRowIndex++
|
|
403
|
+
) {
|
|
404
|
+
const splitRow = splitRows[splitRowIndex]!;
|
|
405
|
+
const lineIndexes = this.getLineIndexesForSplitRow(splitRow);
|
|
406
|
+
for (const lineIndex of lineIndexes) {
|
|
407
|
+
rowByLineIndex[lineIndex] = rows.length;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
rows.push({ kind: "split", splitRowIndex });
|
|
411
|
+
if (!this.inlineAnnotationsVisible) continue;
|
|
412
|
+
|
|
413
|
+
for (const lineIndex of lineIndexes) {
|
|
414
|
+
this.pushInlineCommentRows(rows, lineIndex, width);
|
|
415
|
+
this.pushInlineEditorRows(rows, lineIndex, width);
|
|
416
|
+
this.pushInlineExplanationRows(rows, lineIndex, width);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private getLineIndexesForSplitRow(row: SplitDiffRow): number[] {
|
|
422
|
+
if (row.kind === "full") return [row.cell.index];
|
|
423
|
+
const indexes: number[] = [];
|
|
424
|
+
if (row.left) indexes.push(row.left.index);
|
|
425
|
+
if (row.right && row.right.index !== row.left?.index) {
|
|
426
|
+
indexes.push(row.right.index);
|
|
427
|
+
}
|
|
428
|
+
return indexes;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private pushGlobalAnnotationRows(
|
|
432
|
+
rows: AnnotatedDiffRow[],
|
|
433
|
+
width: number,
|
|
434
|
+
): void {
|
|
435
|
+
if (!this.inlineAnnotationsVisible) return;
|
|
436
|
+
|
|
437
|
+
const globalComment = this.comments.get(GLOBAL_COMMENT_KEY);
|
|
438
|
+
if (globalComment) {
|
|
439
|
+
this.pushAnnotationBlock(
|
|
440
|
+
rows,
|
|
441
|
+
"comment",
|
|
442
|
+
globalComment.text,
|
|
443
|
+
width,
|
|
444
|
+
"Overall diff comment",
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (this.editMode && this.editingCommentKey === GLOBAL_COMMENT_KEY) {
|
|
449
|
+
this.pushInlineEditorBlock(rows, "Draft overall diff note", width);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private pushInlineCommentRows(
|
|
454
|
+
rows: AnnotatedDiffRow[],
|
|
455
|
+
lineIndex: number,
|
|
456
|
+
width: number,
|
|
457
|
+
): void {
|
|
458
|
+
for (const comment of this.getCommentsEndingAtLine(lineIndex)) {
|
|
459
|
+
if (this.editMode && comment.id === this.editingCommentKey) continue;
|
|
460
|
+
this.pushAnnotationBlock(
|
|
461
|
+
rows,
|
|
462
|
+
"comment",
|
|
463
|
+
comment.text,
|
|
464
|
+
width,
|
|
465
|
+
formatCommentLocation(comment),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
306
468
|
}
|
|
307
469
|
|
|
308
|
-
private
|
|
309
|
-
|
|
310
|
-
|
|
470
|
+
private pushInlineEditorRows(
|
|
471
|
+
rows: AnnotatedDiffRow[],
|
|
472
|
+
lineIndex: number,
|
|
473
|
+
width: number,
|
|
474
|
+
): void {
|
|
475
|
+
if (!this.editMode || this.editingCommentKey === GLOBAL_COMMENT_KEY) return;
|
|
476
|
+
const selection = this.getActiveCommentSelection();
|
|
477
|
+
if (!selection || selection.end !== lineIndex) return;
|
|
478
|
+
this.pushInlineEditorBlock(
|
|
479
|
+
rows,
|
|
480
|
+
`Draft note - ${this.formatSelectionLocation(selection)}`,
|
|
481
|
+
width,
|
|
311
482
|
);
|
|
312
483
|
}
|
|
313
484
|
|
|
314
|
-
private
|
|
315
|
-
|
|
316
|
-
|
|
485
|
+
private pushInlineExplanationRows(
|
|
486
|
+
rows: AnnotatedDiffRow[],
|
|
487
|
+
lineIndex: number,
|
|
488
|
+
width: number,
|
|
489
|
+
): void {
|
|
490
|
+
const scope = getCurrentHunkScope(this.lines, lineIndex);
|
|
491
|
+
if (!scope || !this.visibleExplanationKeys.has(scope.key)) return;
|
|
492
|
+
|
|
493
|
+
const end = this.getHunkEndIndex(lineIndex);
|
|
494
|
+
if (end !== lineIndex) return;
|
|
495
|
+
|
|
496
|
+
const explanation = this.explanationController.getState(scope);
|
|
497
|
+
if (!this.explanationController.isAvailable) {
|
|
498
|
+
this.pushExplanationBlock(rows, "Explanation unavailable.", width);
|
|
499
|
+
} else if (!explanation) {
|
|
500
|
+
this.pushExplanationBlock(rows, "No explanation generated yet.", width);
|
|
501
|
+
} else if (explanation.status === "loading") {
|
|
502
|
+
this.pushExplanationBlock(
|
|
503
|
+
rows,
|
|
504
|
+
`${this.explanationController.getLoadingFrame()} ${explanation.text || "Generating explanation..."}`,
|
|
505
|
+
width,
|
|
506
|
+
);
|
|
507
|
+
} else if (explanation.status === "error") {
|
|
508
|
+
this.pushExplanationBlock(
|
|
509
|
+
rows,
|
|
510
|
+
`Explanation failed: ${explanation.message}`,
|
|
511
|
+
width,
|
|
512
|
+
);
|
|
513
|
+
} else {
|
|
514
|
+
this.pushExplanationBlock(rows, explanation.text, width);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
317
517
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
518
|
+
private pushAnnotationBlock(
|
|
519
|
+
rows: AnnotatedDiffRow[],
|
|
520
|
+
kind: "comment",
|
|
521
|
+
text: string,
|
|
522
|
+
width: number,
|
|
523
|
+
title?: string,
|
|
524
|
+
): void {
|
|
525
|
+
this.pushCommentBlock(rows, text, width, title);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private pushCommentBlock(
|
|
529
|
+
rows: AnnotatedDiffRow[],
|
|
530
|
+
text: string,
|
|
531
|
+
width: number,
|
|
532
|
+
title?: string,
|
|
533
|
+
): void {
|
|
534
|
+
rows.push({
|
|
535
|
+
kind: "comment",
|
|
536
|
+
text: title ? this.theme.fg("accent", ` ${title} `) : "",
|
|
537
|
+
part: "top",
|
|
538
|
+
});
|
|
539
|
+
const wrapped = wrapTextWithAnsi(
|
|
540
|
+
this.theme.fg("text", text),
|
|
541
|
+
this.getInlineContentWidth(width),
|
|
542
|
+
);
|
|
543
|
+
for (const line of wrapped.length > 0 ? wrapped : [""]) {
|
|
544
|
+
rows.push({ kind: "comment", text: line, part: "body" });
|
|
545
|
+
}
|
|
546
|
+
rows.push({ kind: "comment", text: "", part: "bottom" });
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private pushExplanationBlock(
|
|
550
|
+
rows: AnnotatedDiffRow[],
|
|
551
|
+
text: string,
|
|
552
|
+
width: number,
|
|
553
|
+
): void {
|
|
554
|
+
rows.push({
|
|
555
|
+
kind: "explanation",
|
|
556
|
+
text: this.theme.fg("accent", " ✨ Explanation "),
|
|
557
|
+
part: "top",
|
|
558
|
+
});
|
|
559
|
+
const wrapped = wrapTextWithAnsi(
|
|
560
|
+
this.theme.fg("muted", text),
|
|
561
|
+
this.getInlineContentWidth(width),
|
|
562
|
+
);
|
|
563
|
+
for (const line of wrapped.length > 0 ? wrapped : [""]) {
|
|
564
|
+
rows.push({ kind: "explanation", text: line, part: "body" });
|
|
565
|
+
}
|
|
566
|
+
rows.push({ kind: "explanation", text: "", part: "bottom" });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private pushInlineEditorBlock(
|
|
570
|
+
rows: AnnotatedDiffRow[],
|
|
571
|
+
title: string,
|
|
572
|
+
width: number,
|
|
573
|
+
): void {
|
|
574
|
+
rows.push({
|
|
575
|
+
kind: "editor",
|
|
576
|
+
text: this.theme.fg("accent", ` ${title} `),
|
|
577
|
+
part: "top",
|
|
578
|
+
});
|
|
579
|
+
const editorLines = this.editor.render(this.getInlineContentWidth(width));
|
|
580
|
+
const bodyLines = editorLines.slice(1, -1);
|
|
581
|
+
for (const line of bodyLines.length > 0 ? bodyLines : [""]) {
|
|
582
|
+
rows.push({ kind: "editor", text: line, part: "body" });
|
|
583
|
+
}
|
|
584
|
+
rows.push({ kind: "editor", text: "", part: "bottom" });
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private renderInlineAnnotationRow(
|
|
588
|
+
row: Exclude<AnnotatedDiffRow, { kind: "diff" } | { kind: "split" }>,
|
|
589
|
+
width: number,
|
|
590
|
+
): string {
|
|
591
|
+
if (row.kind === "editor") {
|
|
592
|
+
return this.renderInlineBoxRow(
|
|
593
|
+
{ text: row.text, part: row.part },
|
|
594
|
+
width,
|
|
595
|
+
"accent",
|
|
331
596
|
);
|
|
332
597
|
}
|
|
333
598
|
|
|
334
|
-
|
|
599
|
+
if (row.kind === "comment" || row.kind === "explanation") {
|
|
600
|
+
return this.renderInlineBoxRow(
|
|
601
|
+
{ text: row.text, part: row.part },
|
|
602
|
+
width,
|
|
603
|
+
"borderMuted",
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return " ".repeat(width);
|
|
335
608
|
}
|
|
336
609
|
|
|
337
|
-
private
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
610
|
+
private renderInlineBoxRow(
|
|
611
|
+
row: { text: string; part: "top" | "body" | "bottom" },
|
|
612
|
+
width: number,
|
|
613
|
+
borderColor: "accent" | "borderMuted",
|
|
614
|
+
): string {
|
|
615
|
+
const indent = " ";
|
|
616
|
+
const contentWidth = this.getInlineContentWidth(width);
|
|
617
|
+
const body =
|
|
618
|
+
row.part === "top" || row.part === "bottom"
|
|
619
|
+
? this.renderInlineBoxHorizontal(row.text, contentWidth, borderColor)
|
|
620
|
+
: padToWidth(truncateToWidth(row.text, contentWidth), contentWidth);
|
|
621
|
+
const left = row.part === "top" ? "╭" : row.part === "bottom" ? "╰" : "│";
|
|
622
|
+
const right = row.part === "top" ? "╮" : row.part === "bottom" ? "╯" : "│";
|
|
623
|
+
return padToWidth(
|
|
624
|
+
truncateToWidth(
|
|
625
|
+
`${indent}${this.theme.fg(borderColor, left)}${body}${this.theme.fg(borderColor, right)}`,
|
|
626
|
+
width,
|
|
627
|
+
),
|
|
628
|
+
width,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
343
631
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
632
|
+
private renderInlineBoxHorizontal(
|
|
633
|
+
title: string,
|
|
634
|
+
width: number,
|
|
635
|
+
borderColor: "accent" | "borderMuted",
|
|
636
|
+
): string {
|
|
637
|
+
if (!title) return this.theme.fg(borderColor, "─".repeat(width));
|
|
350
638
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
splitRow.cell.index,
|
|
356
|
-
width,
|
|
357
|
-
splitRow.cell.index === this.selected,
|
|
358
|
-
this.getSelectionBounds(),
|
|
359
|
-
),
|
|
360
|
-
);
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
639
|
+
const visibleTitle = truncateToWidth(title, Math.max(0, width));
|
|
640
|
+
const remaining = Math.max(0, width - visibleWidth(visibleTitle));
|
|
641
|
+
return `${visibleTitle}${this.theme.fg(borderColor, "─".repeat(remaining))}`;
|
|
642
|
+
}
|
|
363
643
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
644
|
+
private getInlineContentWidth(width: number): number {
|
|
645
|
+
return Math.max(10, width - 8);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private invalidateAnnotatedRows(): void {
|
|
649
|
+
this.annotatedRows = undefined;
|
|
650
|
+
this.annotatedRowByLineIndex = undefined;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private getCommentsEndingAtLine(lineIndex: number): ReviewComment[] {
|
|
654
|
+
const comments: ReviewComment[] = [];
|
|
655
|
+
for (const comment of this.comments.values()) {
|
|
656
|
+
if (comment.global) continue;
|
|
657
|
+
const start = this.lineIndexById.get(comment.startLineId);
|
|
658
|
+
const end = this.lineIndexById.get(comment.endLineId);
|
|
659
|
+
if (start == null || end == null) continue;
|
|
660
|
+
if (Math.max(start, end) === lineIndex) comments.push(comment);
|
|
661
|
+
}
|
|
662
|
+
return comments.sort((a, b) => a.id.localeCompare(b.id));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private getHunkEndIndex(selected: number): number | undefined {
|
|
666
|
+
const selectedLine = this.lines[selected];
|
|
667
|
+
if (!selectedLine?.filePath || !selectedLine.hunkLabel) return undefined;
|
|
668
|
+
|
|
669
|
+
let end = selected;
|
|
670
|
+
while (
|
|
671
|
+
end + 1 < this.lines.length &&
|
|
672
|
+
this.lines[end + 1]?.filePath === selectedLine.filePath &&
|
|
673
|
+
this.lines[end + 1]?.hunkLabel === selectedLine.hunkLabel
|
|
674
|
+
) {
|
|
675
|
+
end++;
|
|
676
|
+
}
|
|
677
|
+
return end;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private renderSplitDiffRowAt(splitRowIndex: number, width: number): string {
|
|
681
|
+
const splitRow = this.getSplitDiffRows()[splitRowIndex];
|
|
682
|
+
if (!splitRow) return " ".repeat(width);
|
|
683
|
+
|
|
684
|
+
if (splitRow.kind === "full") {
|
|
685
|
+
return this.renderDiffLine(
|
|
686
|
+
splitRow.cell.line,
|
|
687
|
+
splitRow.cell.index,
|
|
688
|
+
width,
|
|
689
|
+
splitRow.cell.index === this.selected,
|
|
690
|
+
this.getSelectionBounds(),
|
|
375
691
|
);
|
|
376
692
|
}
|
|
377
693
|
|
|
378
|
-
|
|
694
|
+
const separatorWidth = 3;
|
|
695
|
+
const leftWidth = Math.max(10, Math.floor((width - separatorWidth) / 2));
|
|
696
|
+
const rightWidth = Math.max(10, width - leftWidth - separatorWidth);
|
|
697
|
+
const left = splitRow.left
|
|
698
|
+
? this.renderSplitDiffCell(splitRow.left, leftWidth, "left")
|
|
699
|
+
: " ".repeat(leftWidth);
|
|
700
|
+
const right = splitRow.right
|
|
701
|
+
? this.renderSplitDiffCell(splitRow.right, rightWidth, "right")
|
|
702
|
+
: " ".repeat(rightWidth);
|
|
703
|
+
return truncateToWidth(
|
|
704
|
+
`${padToWidth(left, leftWidth)}${this.theme.fg("borderMuted", " │ ")}${padToWidth(right, rightWidth)}`,
|
|
705
|
+
width,
|
|
706
|
+
);
|
|
379
707
|
}
|
|
380
708
|
|
|
381
709
|
private getContentHeight(): number {
|
|
@@ -387,6 +715,7 @@ export class ReviewComponent {
|
|
|
387
715
|
|
|
388
716
|
invalidate(): void {
|
|
389
717
|
this.highlightedLineCache.clear();
|
|
718
|
+
this.invalidateAnnotatedRows();
|
|
390
719
|
}
|
|
391
720
|
|
|
392
721
|
dispose(): void {
|
|
@@ -409,23 +738,28 @@ export class ReviewComponent {
|
|
|
409
738
|
this.tui.requestRender(true);
|
|
410
739
|
}
|
|
411
740
|
|
|
412
|
-
private
|
|
413
|
-
this.
|
|
741
|
+
private toggleInlineAnnotations(): void {
|
|
742
|
+
this.inlineAnnotationsVisible = !this.inlineAnnotationsVisible;
|
|
743
|
+
this.invalidateAnnotatedRows();
|
|
414
744
|
this.tui.requestRender(true);
|
|
415
745
|
}
|
|
416
746
|
|
|
417
|
-
private
|
|
418
|
-
this.
|
|
419
|
-
this.rightPaneMode = "comments";
|
|
747
|
+
private hideInlineExplanation(): void {
|
|
748
|
+
this.visibleExplanationKeys.clear();
|
|
420
749
|
}
|
|
421
750
|
|
|
422
751
|
private toggleExplanationPane(): void {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (this.
|
|
752
|
+
const scope = this.getCurrentHunkScope();
|
|
753
|
+
if (!scope) return;
|
|
754
|
+
|
|
755
|
+
if (this.visibleExplanationKeys.has(scope.key)) {
|
|
756
|
+
this.visibleExplanationKeys.delete(scope.key);
|
|
757
|
+
} else {
|
|
758
|
+
this.visibleExplanationKeys.add(scope.key);
|
|
427
759
|
this.ensureCurrentExplanation();
|
|
428
760
|
}
|
|
761
|
+
|
|
762
|
+
this.invalidateAnnotatedRows();
|
|
429
763
|
this.tui.requestRender(true);
|
|
430
764
|
}
|
|
431
765
|
|
|
@@ -464,6 +798,16 @@ export class ReviewComponent {
|
|
|
464
798
|
return getSelectionKey(this.lines, start, end);
|
|
465
799
|
}
|
|
466
800
|
|
|
801
|
+
private formatSelectionLocation(selection: SelectionBounds): string {
|
|
802
|
+
const startLine = this.lines[selection.start];
|
|
803
|
+
const endLine = this.lines[selection.end];
|
|
804
|
+
if (!startLine || !endLine) return "diff";
|
|
805
|
+
|
|
806
|
+
const start = formatLocation(startLine);
|
|
807
|
+
const end = formatLocation(endLine);
|
|
808
|
+
return start === end ? start : `${start} -> ${end}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
467
811
|
private getCommentForSelection(
|
|
468
812
|
selection: SelectionBounds | undefined,
|
|
469
813
|
): ReviewComment | undefined {
|
|
@@ -480,6 +824,7 @@ export class ReviewComponent {
|
|
|
480
824
|
|
|
481
825
|
private markCommentsChanged(): void {
|
|
482
826
|
this.commentsRevision++;
|
|
827
|
+
this.invalidateAnnotatedRows();
|
|
483
828
|
this.onCommentsChanged?.(this.comments);
|
|
484
829
|
}
|
|
485
830
|
|
|
@@ -536,10 +881,12 @@ export class ReviewComponent {
|
|
|
536
881
|
|
|
537
882
|
private startGlobalEditMode(): void {
|
|
538
883
|
const existing = this.comments.get(GLOBAL_COMMENT_KEY);
|
|
539
|
-
this.
|
|
884
|
+
this.inlineAnnotationsVisible = true;
|
|
885
|
+
this.hideInlineExplanation();
|
|
540
886
|
this.editMode = true;
|
|
541
887
|
this.editingCommentKey = GLOBAL_COMMENT_KEY;
|
|
542
888
|
this.editor.setText(existing?.text ?? "");
|
|
889
|
+
this.invalidateAnnotatedRows();
|
|
543
890
|
this.tui.requestRender(true);
|
|
544
891
|
}
|
|
545
892
|
|
|
@@ -552,13 +899,15 @@ export class ReviewComponent {
|
|
|
552
899
|
if (startLine.filePath !== endLine.filePath) return;
|
|
553
900
|
|
|
554
901
|
const existing = this.getCommentForSelection(selection);
|
|
555
|
-
this.
|
|
902
|
+
this.inlineAnnotationsVisible = true;
|
|
903
|
+
this.hideInlineExplanation();
|
|
556
904
|
this.editMode = true;
|
|
557
905
|
this.editingCommentKey = this.getSelectionKey(
|
|
558
906
|
selection.start,
|
|
559
907
|
selection.end,
|
|
560
908
|
);
|
|
561
909
|
this.editor.setText(existing?.text ?? "");
|
|
910
|
+
this.invalidateAnnotatedRows();
|
|
562
911
|
this.tui.requestRender(true);
|
|
563
912
|
}
|
|
564
913
|
|
|
@@ -566,28 +915,25 @@ export class ReviewComponent {
|
|
|
566
915
|
this.editMode = false;
|
|
567
916
|
this.editingCommentKey = undefined;
|
|
568
917
|
this.editor.setText("");
|
|
918
|
+
this.invalidateAnnotatedRows();
|
|
569
919
|
this.tui.requestRender(true);
|
|
570
920
|
}
|
|
571
921
|
|
|
572
922
|
private getSplitDiffRows(): SplitDiffRow[] {
|
|
573
923
|
if (this.splitRows) return this.splitRows;
|
|
574
924
|
|
|
575
|
-
const { rows
|
|
925
|
+
const { rows } = buildSplitDiffRows(this.lines);
|
|
576
926
|
this.splitRows = rows;
|
|
577
|
-
this.splitRowByLineIndex = rowByLineIndex;
|
|
578
927
|
return rows;
|
|
579
928
|
}
|
|
580
929
|
|
|
581
|
-
private getSelectedDisplayRow(): number {
|
|
582
|
-
|
|
583
|
-
this.
|
|
584
|
-
return this.splitRowByLineIndex?.[this.selected] ?? 0;
|
|
930
|
+
private getSelectedDisplayRow(width: number): number {
|
|
931
|
+
this.getAnnotatedRows(width);
|
|
932
|
+
return this.annotatedRowByLineIndex?.[this.selected] ?? 0;
|
|
585
933
|
}
|
|
586
934
|
|
|
587
|
-
private getDisplayRowCount(): number {
|
|
588
|
-
return this.
|
|
589
|
-
? this.lines.length
|
|
590
|
-
: this.getSplitDiffRows().length;
|
|
935
|
+
private getDisplayRowCount(width: number): number {
|
|
936
|
+
return this.getAnnotatedRows(width).length;
|
|
591
937
|
}
|
|
592
938
|
|
|
593
939
|
private renderSplitDiffCell(
|
|
@@ -597,7 +943,7 @@ export class ReviewComponent {
|
|
|
597
943
|
): string {
|
|
598
944
|
const { line, index } = cell;
|
|
599
945
|
const hasComment = this.getCommentKeysForLine(index).length > 0;
|
|
600
|
-
const commentMark = hasComment ? this.theme.fg("
|
|
946
|
+
const commentMark = hasComment ? this.theme.fg("borderAccent", "│") : " ";
|
|
601
947
|
const lineNumber =
|
|
602
948
|
side === "left" ? line.oldLineNumber : line.newLineNumber;
|
|
603
949
|
const prefix = `${commentMark} ${lineNumberCell(lineNumber)} `;
|
|
@@ -613,11 +959,11 @@ export class ReviewComponent {
|
|
|
613
959
|
return this.applyDiffBackground(line, styled, width);
|
|
614
960
|
}
|
|
615
961
|
|
|
616
|
-
private ensureScroll(viewportHeight: number): void {
|
|
962
|
+
private ensureScroll(viewportHeight: number, width: number): void {
|
|
617
963
|
this.navigation.ensureScroll(
|
|
618
964
|
viewportHeight,
|
|
619
|
-
this.getSelectedDisplayRow(),
|
|
620
|
-
this.getDisplayRowCount(),
|
|
965
|
+
this.getSelectedDisplayRow(width),
|
|
966
|
+
this.getDisplayRowCount(width),
|
|
621
967
|
);
|
|
622
968
|
}
|
|
623
969
|
|
|
@@ -686,7 +1032,7 @@ export class ReviewComponent {
|
|
|
686
1032
|
selection?: SelectionBounds,
|
|
687
1033
|
): string {
|
|
688
1034
|
const hasComment = this.getCommentKeysForLine(index).length > 0;
|
|
689
|
-
const commentMark = hasComment ? this.theme.fg("
|
|
1035
|
+
const commentMark = hasComment ? this.theme.fg("borderAccent", "│") : " ";
|
|
690
1036
|
const numbers = `${lineNumberCell(line.oldLineNumber)} ${lineNumberCell(line.newLineNumber)}`;
|
|
691
1037
|
const prefix = `${commentMark} ${numbers} `;
|
|
692
1038
|
let styled = this.renderDiffRowContent(line, prefix);
|
|
@@ -700,54 +1046,6 @@ export class ReviewComponent {
|
|
|
700
1046
|
return this.applyDiffBackground(line, styled, width);
|
|
701
1047
|
}
|
|
702
1048
|
|
|
703
|
-
private renderRightPane(
|
|
704
|
-
width: number,
|
|
705
|
-
height: number,
|
|
706
|
-
selectedLine?: ReviewLine,
|
|
707
|
-
): string[] {
|
|
708
|
-
return this.rightPaneMode === "explanation"
|
|
709
|
-
? this.renderExplanationPane(width, height, selectedLine)
|
|
710
|
-
: this.renderCommentsPane(width, height, selectedLine);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
private renderCommentsPane(
|
|
714
|
-
width: number,
|
|
715
|
-
height: number,
|
|
716
|
-
selectedLine?: ReviewLine,
|
|
717
|
-
): string[] {
|
|
718
|
-
const selection = this.getActiveCommentSelection();
|
|
719
|
-
return renderCommentsPaneContent({
|
|
720
|
-
width,
|
|
721
|
-
height,
|
|
722
|
-
selectedLine,
|
|
723
|
-
theme: this.theme,
|
|
724
|
-
lines: this.lines,
|
|
725
|
-
comments: this.comments,
|
|
726
|
-
editor: this.editor,
|
|
727
|
-
editMode: this.editMode,
|
|
728
|
-
editingCommentKey: this.editingCommentKey,
|
|
729
|
-
selection,
|
|
730
|
-
currentComment: this.getCommentForSelection(selection),
|
|
731
|
-
footerText: this.getFooterText(selectedLine),
|
|
732
|
-
hasSelection: this.hasSelection(),
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
private renderExplanationPane(
|
|
737
|
-
width: number,
|
|
738
|
-
height: number,
|
|
739
|
-
selectedLine?: ReviewLine,
|
|
740
|
-
): string[] {
|
|
741
|
-
return renderExplanationPaneContent({
|
|
742
|
-
width,
|
|
743
|
-
height,
|
|
744
|
-
selectedLine,
|
|
745
|
-
theme: this.theme,
|
|
746
|
-
scope: this.getCurrentHunkScope(),
|
|
747
|
-
controller: this.explanationController,
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
|
|
751
1049
|
private ensureCurrentExplanation(): void {
|
|
752
1050
|
this.explanationController.ensure(this.getCurrentHunkScope());
|
|
753
1051
|
}
|
package/src/types.ts
CHANGED
package/src/review-panes.ts
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
-
import type { Editor } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { ExplanationController } from "./explanation-controller.ts";
|
|
4
|
-
import { GLOBAL_COMMENT_KEY } from "./comment-manager.ts";
|
|
5
|
-
import { formatLocation } from "./prompt.ts";
|
|
6
|
-
import type { ExplanationScope } from "./explain.ts";
|
|
7
|
-
import type {
|
|
8
|
-
ReviewComment,
|
|
9
|
-
ReviewLine,
|
|
10
|
-
ReviewTheme,
|
|
11
|
-
SelectionBounds,
|
|
12
|
-
} from "./types.ts";
|
|
13
|
-
|
|
14
|
-
export type RenderCommentsPaneOptions = {
|
|
15
|
-
width: number;
|
|
16
|
-
height: number;
|
|
17
|
-
selectedLine?: ReviewLine;
|
|
18
|
-
theme: ReviewTheme;
|
|
19
|
-
lines: ReviewLine[];
|
|
20
|
-
comments: Map<string, ReviewComment>;
|
|
21
|
-
editor: Editor;
|
|
22
|
-
editMode: boolean;
|
|
23
|
-
editingCommentKey?: string;
|
|
24
|
-
selection: SelectionBounds | undefined;
|
|
25
|
-
currentComment: ReviewComment | undefined;
|
|
26
|
-
footerText: string;
|
|
27
|
-
hasSelection: boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function renderCommentsPane({
|
|
31
|
-
width,
|
|
32
|
-
height,
|
|
33
|
-
selectedLine,
|
|
34
|
-
theme,
|
|
35
|
-
lines: diffLines,
|
|
36
|
-
comments,
|
|
37
|
-
editor,
|
|
38
|
-
editMode,
|
|
39
|
-
editingCommentKey,
|
|
40
|
-
selection,
|
|
41
|
-
currentComment,
|
|
42
|
-
footerText,
|
|
43
|
-
hasSelection,
|
|
44
|
-
}: RenderCommentsPaneOptions): string[] {
|
|
45
|
-
const lines: string[] = [];
|
|
46
|
-
const title = theme.fg("accent", theme.bold("Comments"));
|
|
47
|
-
|
|
48
|
-
lines.push(truncateToWidth(title, width));
|
|
49
|
-
lines.push(
|
|
50
|
-
truncateToWidth(
|
|
51
|
-
theme.fg(
|
|
52
|
-
"dim",
|
|
53
|
-
selection
|
|
54
|
-
? footerText
|
|
55
|
-
: selectedLine
|
|
56
|
-
? formatLocation(selectedLine)
|
|
57
|
-
: "No selection",
|
|
58
|
-
),
|
|
59
|
-
width,
|
|
60
|
-
),
|
|
61
|
-
);
|
|
62
|
-
lines.push("");
|
|
63
|
-
|
|
64
|
-
if (editMode && editingCommentKey === GLOBAL_COMMENT_KEY) {
|
|
65
|
-
lines[1] = truncateToWidth(theme.fg("dim", "Overall diff comment"), width);
|
|
66
|
-
lines.push(
|
|
67
|
-
...wrapTextWithAnsi(
|
|
68
|
-
theme.fg(
|
|
69
|
-
"dim",
|
|
70
|
-
"Editing overall diff comment. Enter saves. Esc or Ctrl+C cancels.",
|
|
71
|
-
),
|
|
72
|
-
width,
|
|
73
|
-
),
|
|
74
|
-
);
|
|
75
|
-
lines.push("");
|
|
76
|
-
for (const line of editor.render(Math.max(10, width))) {
|
|
77
|
-
lines.push(truncateToWidth(line, width));
|
|
78
|
-
}
|
|
79
|
-
return lines.slice(0, height);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const globalComment = comments.get(GLOBAL_COMMENT_KEY);
|
|
83
|
-
if (globalComment) {
|
|
84
|
-
lines.push(
|
|
85
|
-
truncateToWidth(
|
|
86
|
-
theme.fg("accent", theme.bold("Overall diff comment")),
|
|
87
|
-
width,
|
|
88
|
-
),
|
|
89
|
-
);
|
|
90
|
-
lines.push(
|
|
91
|
-
...wrapTextWithAnsi(theme.fg("text", globalComment.text), width),
|
|
92
|
-
);
|
|
93
|
-
lines.push(...wrapTextWithAnsi(theme.fg("dim", "C edits"), width));
|
|
94
|
-
lines.push("");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (!selectedLine) {
|
|
98
|
-
lines.push(
|
|
99
|
-
...wrapTextWithAnsi(theme.fg("muted", "No diff lines available."), width),
|
|
100
|
-
);
|
|
101
|
-
return lines.slice(0, height);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!selection) {
|
|
105
|
-
lines.push(
|
|
106
|
-
...wrapTextWithAnsi(
|
|
107
|
-
theme.fg(
|
|
108
|
-
"muted",
|
|
109
|
-
"Move to a diff line and press c to add a comment, or press C for an overall diff comment.",
|
|
110
|
-
),
|
|
111
|
-
width,
|
|
112
|
-
),
|
|
113
|
-
);
|
|
114
|
-
return lines.slice(0, height);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (editMode && currentComment?.id === editingCommentKey) {
|
|
118
|
-
lines.push(
|
|
119
|
-
...wrapTextWithAnsi(
|
|
120
|
-
theme.fg("dim", "Editing comment. Enter saves. Esc or Ctrl+C cancels."),
|
|
121
|
-
width,
|
|
122
|
-
),
|
|
123
|
-
);
|
|
124
|
-
lines.push("");
|
|
125
|
-
for (const line of editor.render(Math.max(10, width))) {
|
|
126
|
-
lines.push(truncateToWidth(line, width));
|
|
127
|
-
}
|
|
128
|
-
} else if (editMode && editingCommentKey) {
|
|
129
|
-
lines.push(
|
|
130
|
-
...wrapTextWithAnsi(
|
|
131
|
-
theme.fg("dim", "Editing comment. Enter saves. Esc or Ctrl+C cancels."),
|
|
132
|
-
width,
|
|
133
|
-
),
|
|
134
|
-
);
|
|
135
|
-
lines.push("");
|
|
136
|
-
for (const line of editor.render(Math.max(10, width))) {
|
|
137
|
-
lines.push(truncateToWidth(line, width));
|
|
138
|
-
}
|
|
139
|
-
} else if (currentComment) {
|
|
140
|
-
lines.push(
|
|
141
|
-
...wrapTextWithAnsi(theme.fg("text", currentComment.text), width),
|
|
142
|
-
);
|
|
143
|
-
lines.push("");
|
|
144
|
-
lines.push(
|
|
145
|
-
...wrapTextWithAnsi(
|
|
146
|
-
theme.fg("dim", "x deletes this comment • c edits"),
|
|
147
|
-
width,
|
|
148
|
-
),
|
|
149
|
-
);
|
|
150
|
-
} else {
|
|
151
|
-
lines.push(
|
|
152
|
-
...wrapTextWithAnsi(
|
|
153
|
-
theme.fg(
|
|
154
|
-
"muted",
|
|
155
|
-
hasSelection
|
|
156
|
-
? "No comment on this range."
|
|
157
|
-
: "No comment on this line.",
|
|
158
|
-
),
|
|
159
|
-
width,
|
|
160
|
-
),
|
|
161
|
-
);
|
|
162
|
-
lines.push("");
|
|
163
|
-
lines.push(
|
|
164
|
-
...wrapTextWithAnsi(
|
|
165
|
-
theme.fg(
|
|
166
|
-
"dim",
|
|
167
|
-
hasSelection
|
|
168
|
-
? "Press c to add a range comment, or C for an overall diff comment."
|
|
169
|
-
: "Press c to add one. Use J/K to extend a range. Press C for an overall diff comment.",
|
|
170
|
-
),
|
|
171
|
-
width,
|
|
172
|
-
),
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
lines.push("");
|
|
177
|
-
lines.push(truncateToWidth(theme.fg("accent", theme.bold("Excerpt")), width));
|
|
178
|
-
const excerpt = diffLines
|
|
179
|
-
.slice(selection.start, selection.end + 1)
|
|
180
|
-
.map((line) => line.text)
|
|
181
|
-
.join("\n");
|
|
182
|
-
lines.push(
|
|
183
|
-
...wrapTextWithAnsi(
|
|
184
|
-
theme.fg("toolDiffContext", excerpt || "(blank line)"),
|
|
185
|
-
width,
|
|
186
|
-
),
|
|
187
|
-
);
|
|
188
|
-
return lines.slice(0, height);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export type RenderExplanationPaneOptions = {
|
|
192
|
-
width: number;
|
|
193
|
-
height: number;
|
|
194
|
-
selectedLine?: ReviewLine;
|
|
195
|
-
theme: ReviewTheme;
|
|
196
|
-
scope: ExplanationScope | undefined;
|
|
197
|
-
controller: ExplanationController;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export function renderExplanationPane({
|
|
201
|
-
width,
|
|
202
|
-
height,
|
|
203
|
-
selectedLine,
|
|
204
|
-
theme,
|
|
205
|
-
scope,
|
|
206
|
-
controller,
|
|
207
|
-
}: RenderExplanationPaneOptions): string[] {
|
|
208
|
-
const lines: string[] = [];
|
|
209
|
-
const title = theme.fg("accent", theme.bold("Explanation"));
|
|
210
|
-
|
|
211
|
-
lines.push(truncateToWidth(title, width));
|
|
212
|
-
lines.push(
|
|
213
|
-
truncateToWidth(
|
|
214
|
-
theme.fg(
|
|
215
|
-
"dim",
|
|
216
|
-
scope?.title ??
|
|
217
|
-
(selectedLine ? formatLocation(selectedLine) : "No selection"),
|
|
218
|
-
),
|
|
219
|
-
width,
|
|
220
|
-
),
|
|
221
|
-
);
|
|
222
|
-
lines.push("");
|
|
223
|
-
|
|
224
|
-
if (!controller.isAvailable) {
|
|
225
|
-
lines.push(
|
|
226
|
-
...wrapTextWithAnsi(
|
|
227
|
-
theme.fg("warning", "Diff explanations are unavailable."),
|
|
228
|
-
width,
|
|
229
|
-
),
|
|
230
|
-
);
|
|
231
|
-
return lines.slice(0, height);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (!scope) {
|
|
235
|
-
lines.push(
|
|
236
|
-
...wrapTextWithAnsi(
|
|
237
|
-
theme.fg(
|
|
238
|
-
"muted",
|
|
239
|
-
"Move to a changed hunk and press ? to generate an explanation.",
|
|
240
|
-
),
|
|
241
|
-
width,
|
|
242
|
-
),
|
|
243
|
-
);
|
|
244
|
-
return lines.slice(0, height);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const explanation = controller.getState(scope);
|
|
248
|
-
if (!explanation) {
|
|
249
|
-
lines.push(
|
|
250
|
-
...wrapTextWithAnsi(
|
|
251
|
-
theme.fg(
|
|
252
|
-
"muted",
|
|
253
|
-
"No explanation generated yet. Press ? again after returning to comments to generate this hunk.",
|
|
254
|
-
),
|
|
255
|
-
width,
|
|
256
|
-
),
|
|
257
|
-
);
|
|
258
|
-
} else if (explanation.status === "loading") {
|
|
259
|
-
const spinner = controller.getLoadingFrame();
|
|
260
|
-
lines.push(
|
|
261
|
-
truncateToWidth(
|
|
262
|
-
theme.fg("accent", `${spinner} Generating explanation...`),
|
|
263
|
-
width,
|
|
264
|
-
),
|
|
265
|
-
);
|
|
266
|
-
if (explanation.text.trim()) {
|
|
267
|
-
lines.push("");
|
|
268
|
-
lines.push(
|
|
269
|
-
...wrapTextWithAnsi(theme.fg("text", explanation.text), width),
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
} else if (explanation.status === "error") {
|
|
273
|
-
lines.push(
|
|
274
|
-
...wrapTextWithAnsi(
|
|
275
|
-
theme.fg("warning", `Unable to explain diff: ${explanation.message}`),
|
|
276
|
-
width,
|
|
277
|
-
),
|
|
278
|
-
);
|
|
279
|
-
} else {
|
|
280
|
-
lines.push(...wrapTextWithAnsi(theme.fg("text", explanation.text), width));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
lines.push("");
|
|
284
|
-
lines.push(...wrapTextWithAnsi(theme.fg("dim", "? comments"), width));
|
|
285
|
-
return lines.slice(0, height);
|
|
286
|
-
}
|