pi-diff-review 0.1.12 → 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 +36 -4
- package/package.json +1 -1
- package/src/comment-manager.ts +73 -0
- package/src/diff-source.ts +36 -10
- package/src/explanation-controller.ts +167 -0
- package/src/index.ts +70 -0
- package/src/prompt.ts +1 -1
- package/src/render-utils.ts +11 -0
- package/src/review-component.ts +539 -708
- package/src/review-navigation.ts +94 -0
- package/src/split-diff.ts +61 -0
- package/src/types.ts +0 -3
package/src/review-component.ts
CHANGED
|
@@ -9,60 +9,71 @@ import {
|
|
|
9
9
|
visibleWidth,
|
|
10
10
|
wrapTextWithAnsi,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
|
-
import type {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
import type { DiffExplainer } from "./explain.ts";
|
|
13
|
+
import {
|
|
14
|
+
GLOBAL_COMMENT_KEY,
|
|
15
|
+
buildCommentFromSelection,
|
|
16
|
+
buildCommentLineKeys,
|
|
17
|
+
buildGlobalComment,
|
|
18
|
+
getSelectionKey,
|
|
19
|
+
} from "./comment-manager.ts";
|
|
20
|
+
import {
|
|
21
|
+
ExplanationController,
|
|
22
|
+
getCurrentHunkScope,
|
|
23
|
+
} from "./explanation-controller.ts";
|
|
24
|
+
import { formatCommentLocation, formatLocation } from "./prompt.ts";
|
|
25
|
+
import { ReviewNavigationState } from "./review-navigation.ts";
|
|
26
|
+
import { padToWidth, lineNumberCell } from "./render-utils.ts";
|
|
27
|
+
import { buildSplitDiffRows } from "./split-diff.ts";
|
|
18
28
|
import type {
|
|
19
29
|
DiffRenderMode,
|
|
20
30
|
ReviewComment,
|
|
21
|
-
ReviewLayout,
|
|
22
31
|
ReviewLine,
|
|
23
32
|
ReviewResult,
|
|
24
33
|
ReviewTheme,
|
|
25
34
|
ReviewTui,
|
|
26
|
-
RightPaneMode,
|
|
27
35
|
SelectionBounds,
|
|
28
36
|
SplitDiffCell,
|
|
29
37
|
SplitDiffRow,
|
|
30
38
|
} from "./types.ts";
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
};
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const GLOBAL_COMMENT_KEY = "__global_diff_comment__";
|
|
48
|
+
type AnnotatedDiffRow =
|
|
49
|
+
| { kind: "diff"; lineIndex: number }
|
|
50
|
+
| { kind: "split"; splitRowIndex: number }
|
|
51
|
+
| InlineBoxRow;
|
|
43
52
|
|
|
44
53
|
export class ReviewComponent {
|
|
45
|
-
private
|
|
46
|
-
private scrollTop = 0;
|
|
54
|
+
private navigation: ReviewNavigationState;
|
|
47
55
|
private editMode = false;
|
|
48
56
|
private editingCommentKey?: string;
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
private
|
|
52
|
-
private
|
|
53
|
-
private explanations = new Map<string, ExplanationState>();
|
|
54
|
-
private explanationAbort?: AbortController;
|
|
55
|
-
private explanationRequestId = 0;
|
|
56
|
-
private loadingFrame = 0;
|
|
57
|
-
private loadingTimer?: ReturnType<typeof setInterval>;
|
|
57
|
+
|
|
58
|
+
private inlineAnnotationsVisible = true;
|
|
59
|
+
private visibleExplanationKeys = new Set<string>();
|
|
60
|
+
private explanationController: ExplanationController;
|
|
58
61
|
private editor: Editor;
|
|
59
62
|
private splitRows?: SplitDiffRow[];
|
|
60
|
-
private splitRowByLineIndex?: number[];
|
|
61
63
|
private lineIndexById = new Map<string, number>();
|
|
62
64
|
private commentLineKeys = new Map<number, string[]>();
|
|
63
65
|
private commentsRevision = 0;
|
|
64
66
|
private commentLineKeysRevision = -1;
|
|
65
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[];
|
|
66
77
|
|
|
67
78
|
constructor(
|
|
68
79
|
private tui: ReviewTui,
|
|
@@ -73,10 +84,21 @@ export class ReviewComponent {
|
|
|
73
84
|
private done: (result: ReviewResult) => void,
|
|
74
85
|
private explainer?: DiffExplainer,
|
|
75
86
|
private onCommentsChanged?: (comments: Map<string, ReviewComment>) => void,
|
|
87
|
+
cachedExplanations?: Map<string, string>,
|
|
88
|
+
private onExplanationsChanged?: (explanations: Map<string, string>) => void,
|
|
76
89
|
) {
|
|
77
90
|
const firstCommentable = this.lines.findIndex((line) => line.commentable);
|
|
78
|
-
this.
|
|
91
|
+
this.navigation = new ReviewNavigationState(
|
|
92
|
+
this.lines.length,
|
|
93
|
+
firstCommentable >= 0 ? firstCommentable : 0,
|
|
94
|
+
);
|
|
79
95
|
this.lines.forEach((line, index) => this.lineIndexById.set(line.id, index));
|
|
96
|
+
this.explanationController = new ExplanationController(
|
|
97
|
+
tui,
|
|
98
|
+
explainer,
|
|
99
|
+
cachedExplanations,
|
|
100
|
+
onExplanationsChanged,
|
|
101
|
+
);
|
|
80
102
|
|
|
81
103
|
this.editor = new Editor(tui as never, {
|
|
82
104
|
borderColor: (s) => theme.fg("accent", s),
|
|
@@ -96,10 +118,7 @@ export class ReviewComponent {
|
|
|
96
118
|
if (this.comments.delete(GLOBAL_COMMENT_KEY))
|
|
97
119
|
this.markCommentsChanged();
|
|
98
120
|
} else {
|
|
99
|
-
this.comments.set(
|
|
100
|
-
GLOBAL_COMMENT_KEY,
|
|
101
|
-
this.buildGlobalComment(trimmed),
|
|
102
|
-
);
|
|
121
|
+
this.comments.set(GLOBAL_COMMENT_KEY, buildGlobalComment(trimmed));
|
|
103
122
|
this.markCommentsChanged();
|
|
104
123
|
}
|
|
105
124
|
this.exitEditMode();
|
|
@@ -118,15 +137,36 @@ export class ReviewComponent {
|
|
|
118
137
|
} else {
|
|
119
138
|
this.comments.set(
|
|
120
139
|
key,
|
|
121
|
-
this.
|
|
140
|
+
buildCommentFromSelection(this.lines, selection, trimmed),
|
|
122
141
|
);
|
|
123
142
|
this.markCommentsChanged();
|
|
124
143
|
}
|
|
125
144
|
|
|
145
|
+
this.navigation.clearSelection();
|
|
126
146
|
this.exitEditMode();
|
|
127
147
|
};
|
|
128
148
|
}
|
|
129
149
|
|
|
150
|
+
private get selected(): number {
|
|
151
|
+
return this.navigation.selected;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private set selected(value: number) {
|
|
155
|
+
this.navigation.selected = value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private get scrollTop(): number {
|
|
159
|
+
return this.navigation.scrollTop;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private set scrollTop(value: number) {
|
|
163
|
+
this.navigation.scrollTop = value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private get diffRenderMode(): DiffRenderMode {
|
|
167
|
+
return this.navigation.diffRenderMode;
|
|
168
|
+
}
|
|
169
|
+
|
|
130
170
|
handleInput(data: string): void {
|
|
131
171
|
if (this.editMode) {
|
|
132
172
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
@@ -134,6 +174,7 @@ export class ReviewComponent {
|
|
|
134
174
|
return;
|
|
135
175
|
}
|
|
136
176
|
this.editor.handleInput(data);
|
|
177
|
+
this.invalidateAnnotatedRows();
|
|
137
178
|
this.tui.requestRender();
|
|
138
179
|
return;
|
|
139
180
|
}
|
|
@@ -151,6 +192,10 @@ export class ReviewComponent {
|
|
|
151
192
|
return;
|
|
152
193
|
}
|
|
153
194
|
if (data === "t") {
|
|
195
|
+
this.toggleInlineAnnotations();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (data === "v") {
|
|
154
199
|
this.toggleDiffRenderMode();
|
|
155
200
|
return;
|
|
156
201
|
}
|
|
@@ -229,27 +274,17 @@ export class ReviewComponent {
|
|
|
229
274
|
this.theme.fg(
|
|
230
275
|
"dim",
|
|
231
276
|
this.editMode
|
|
232
|
-
? `${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`
|
|
233
278
|
: this.hasSelection()
|
|
234
|
-
? `${this.lines.length} lines • ${this.comments.size} comments • J/K extend • Esc clear selection • c comment range • C overall comment • Enter submit`
|
|
235
|
-
: `${this.lines.length} lines • ${this.comments.size} comments • ${this.getPositionText(selectedLine)} • j/k move • g/G top/bottom • ctrl-u/d page • t unified/split • ? explain • J/K extend • c comment • C overall • x delete • n/p hunk • Enter submit • q quit`,
|
|
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`,
|
|
236
281
|
),
|
|
237
282
|
width,
|
|
238
283
|
),
|
|
239
284
|
);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
...this.renderSideBySide(width, viewportHeight, selectedLine),
|
|
244
|
-
);
|
|
245
|
-
} else {
|
|
246
|
-
const { diffHeight, commentsHeight } =
|
|
247
|
-
this.getStackedHeights(viewportHeight);
|
|
248
|
-
this.ensureScroll(diffHeight);
|
|
249
|
-
output.push(
|
|
250
|
-
...this.renderStacked(width, diffHeight, commentsHeight, selectedLine),
|
|
251
|
-
);
|
|
252
|
-
}
|
|
285
|
+
|
|
286
|
+
this.ensureScroll(viewportHeight, width);
|
|
287
|
+
output.push(...this.renderAnnotatedDiffRows(width, viewportHeight));
|
|
253
288
|
|
|
254
289
|
output.push(
|
|
255
290
|
truncateToWidth(
|
|
@@ -260,232 +295,495 @@ export class ReviewComponent {
|
|
|
260
295
|
return output;
|
|
261
296
|
}
|
|
262
297
|
|
|
263
|
-
private
|
|
264
|
-
width
|
|
265
|
-
height: number,
|
|
266
|
-
selectedLine?: ReviewLine,
|
|
267
|
-
): string[] {
|
|
268
|
-
const rightWidth = Math.max(28, Math.floor(width * 0.34));
|
|
269
|
-
const separatorWidth = 3;
|
|
270
|
-
const leftWidth = Math.max(30, width - rightWidth - separatorWidth);
|
|
271
|
-
const rightPane = this.renderRightPane(rightWidth, height, selectedLine);
|
|
298
|
+
private renderAnnotatedDiffRows(width: number, height: number): string[] {
|
|
299
|
+
const rows = this.getAnnotatedRows(width);
|
|
272
300
|
const output: string[] = [];
|
|
273
|
-
const diffPane = this.renderDiffRows(leftWidth, height);
|
|
274
301
|
|
|
275
302
|
for (let row = 0; row < height; row++) {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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));
|
|
280
330
|
}
|
|
281
331
|
|
|
282
332
|
return output;
|
|
283
333
|
}
|
|
284
334
|
|
|
285
|
-
private
|
|
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[],
|
|
286
379
|
width: number,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
this.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
];
|
|
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
|
+
}
|
|
299
391
|
}
|
|
300
392
|
|
|
301
|
-
private
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|
|
305
419
|
}
|
|
306
420
|
|
|
307
|
-
private
|
|
308
|
-
|
|
309
|
-
const
|
|
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
|
+
}
|
|
310
430
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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",
|
|
324
445
|
);
|
|
325
446
|
}
|
|
326
447
|
|
|
327
|
-
|
|
448
|
+
if (this.editMode && this.editingCommentKey === GLOBAL_COMMENT_KEY) {
|
|
449
|
+
this.pushInlineEditorBlock(rows, "Draft overall diff note", width);
|
|
450
|
+
}
|
|
328
451
|
}
|
|
329
452
|
|
|
330
|
-
private
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
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
|
+
}
|
|
468
|
+
}
|
|
336
469
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
343
484
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
+
}
|
|
356
517
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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",
|
|
368
596
|
);
|
|
369
597
|
}
|
|
370
598
|
|
|
371
|
-
|
|
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);
|
|
372
608
|
}
|
|
373
609
|
|
|
374
|
-
private
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
);
|
|
379
630
|
}
|
|
380
631
|
|
|
381
|
-
private
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
let commentsHeight = availableForPanes - diffHeight;
|
|
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));
|
|
388
638
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
+
}
|
|
643
|
+
|
|
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);
|
|
392
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;
|
|
393
668
|
|
|
394
|
-
|
|
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(),
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
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
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private getContentHeight(): number {
|
|
710
|
+
const terminalRows = this.tui.terminal?.rows ?? 24;
|
|
711
|
+
const headerHeight = 3;
|
|
712
|
+
const footerHeight = 2;
|
|
713
|
+
return Math.max(6, terminalRows - headerHeight - footerHeight);
|
|
395
714
|
}
|
|
396
715
|
|
|
397
716
|
invalidate(): void {
|
|
398
717
|
this.highlightedLineCache.clear();
|
|
718
|
+
this.invalidateAnnotatedRows();
|
|
399
719
|
}
|
|
400
720
|
|
|
401
721
|
dispose(): void {
|
|
402
|
-
this.
|
|
403
|
-
this.stopLoadingTimer();
|
|
722
|
+
this.explanationController.dispose();
|
|
404
723
|
}
|
|
405
724
|
|
|
406
725
|
private move(delta: number): void {
|
|
407
|
-
|
|
408
|
-
0,
|
|
409
|
-
Math.min(this.lines.length - 1, this.selected + delta),
|
|
410
|
-
);
|
|
411
|
-
if (next === this.selected) return;
|
|
412
|
-
this.selected = next;
|
|
726
|
+
if (!this.navigation.move(delta)) return;
|
|
413
727
|
this.tui.requestRender();
|
|
414
728
|
}
|
|
415
729
|
|
|
416
730
|
private jumpToBoundary(boundary: "start" | "end"): void {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
this.selected = next;
|
|
421
|
-
if (hadSelection) {
|
|
422
|
-
this.clearSelection();
|
|
423
|
-
} else {
|
|
424
|
-
this.tui.requestRender();
|
|
425
|
-
}
|
|
731
|
+
const result = this.navigation.jumpToBoundary(boundary);
|
|
732
|
+
if (!result.changed) return;
|
|
733
|
+
this.tui.requestRender();
|
|
426
734
|
}
|
|
427
735
|
|
|
428
|
-
private
|
|
429
|
-
this.
|
|
736
|
+
private toggleDiffRenderMode(): void {
|
|
737
|
+
this.navigation.toggleDiffRenderMode();
|
|
430
738
|
this.tui.requestRender(true);
|
|
431
739
|
}
|
|
432
740
|
|
|
433
|
-
private
|
|
434
|
-
this.
|
|
435
|
-
|
|
436
|
-
this.scrollTop = 0;
|
|
741
|
+
private toggleInlineAnnotations(): void {
|
|
742
|
+
this.inlineAnnotationsVisible = !this.inlineAnnotationsVisible;
|
|
743
|
+
this.invalidateAnnotatedRows();
|
|
437
744
|
this.tui.requestRender(true);
|
|
438
745
|
}
|
|
439
746
|
|
|
747
|
+
private hideInlineExplanation(): void {
|
|
748
|
+
this.visibleExplanationKeys.clear();
|
|
749
|
+
}
|
|
750
|
+
|
|
440
751
|
private toggleExplanationPane(): void {
|
|
441
|
-
this.
|
|
442
|
-
|
|
443
|
-
|
|
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);
|
|
444
759
|
this.ensureCurrentExplanation();
|
|
445
760
|
}
|
|
761
|
+
|
|
762
|
+
this.invalidateAnnotatedRows();
|
|
446
763
|
this.tui.requestRender(true);
|
|
447
764
|
}
|
|
448
765
|
|
|
449
766
|
private getPageMoveAmount(): number {
|
|
450
767
|
const contentHeight = this.getContentHeight();
|
|
451
|
-
|
|
452
|
-
this.layout === "stacked"
|
|
453
|
-
? this.getStackedHeights(contentHeight).diffHeight
|
|
454
|
-
: contentHeight;
|
|
455
|
-
return Math.max(1, Math.floor(diffHeight / 2));
|
|
768
|
+
return Math.max(1, Math.floor(contentHeight / 2));
|
|
456
769
|
}
|
|
457
770
|
|
|
458
771
|
private extendSelection(delta: number): void {
|
|
459
|
-
if (this.
|
|
460
|
-
this.selectionAnchor = this.selected;
|
|
461
|
-
}
|
|
462
|
-
const next = Math.max(
|
|
463
|
-
0,
|
|
464
|
-
Math.min(this.lines.length - 1, this.selected + delta),
|
|
465
|
-
);
|
|
466
|
-
if (next === this.selected) return;
|
|
467
|
-
this.selected = next;
|
|
772
|
+
if (!this.navigation.extendSelection(delta)) return;
|
|
468
773
|
this.tui.requestRender();
|
|
469
774
|
}
|
|
470
775
|
|
|
471
776
|
private clearSelection(): void {
|
|
472
|
-
if (this.
|
|
473
|
-
this.selectionAnchor = undefined;
|
|
777
|
+
if (!this.navigation.clearSelection()) return;
|
|
474
778
|
this.tui.requestRender();
|
|
475
779
|
}
|
|
476
780
|
|
|
477
781
|
private hasSelection(): boolean {
|
|
478
|
-
return (
|
|
479
|
-
this.selectionAnchor != null && this.selectionAnchor !== this.selected
|
|
480
|
-
);
|
|
782
|
+
return this.navigation.hasSelection();
|
|
481
783
|
}
|
|
482
784
|
|
|
483
785
|
private getSelectionBounds(): SelectionBounds | undefined {
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
start: Math.min(this.selectionAnchor, this.selected),
|
|
487
|
-
end: Math.max(this.selectionAnchor, this.selected),
|
|
488
|
-
};
|
|
786
|
+
return this.navigation.getSelectionBounds();
|
|
489
787
|
}
|
|
490
788
|
|
|
491
789
|
private getActiveCommentSelection(): SelectionBounds | undefined {
|
|
@@ -497,7 +795,17 @@ export class ReviewComponent {
|
|
|
497
795
|
}
|
|
498
796
|
|
|
499
797
|
private getSelectionKey(start: number, end: number): string {
|
|
500
|
-
return
|
|
798
|
+
return getSelectionKey(this.lines, start, end);
|
|
799
|
+
}
|
|
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}`;
|
|
501
809
|
}
|
|
502
810
|
|
|
503
811
|
private getCommentForSelection(
|
|
@@ -516,68 +824,20 @@ export class ReviewComponent {
|
|
|
516
824
|
|
|
517
825
|
private markCommentsChanged(): void {
|
|
518
826
|
this.commentsRevision++;
|
|
827
|
+
this.invalidateAnnotatedRows();
|
|
519
828
|
this.onCommentsChanged?.(this.comments);
|
|
520
829
|
}
|
|
521
830
|
|
|
522
831
|
private ensureCommentLineKeys(): void {
|
|
523
832
|
if (this.commentLineKeysRevision === this.commentsRevision) return;
|
|
524
833
|
|
|
525
|
-
this.commentLineKeys =
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (start == null || end == null) continue;
|
|
530
|
-
const from = Math.min(start, end);
|
|
531
|
-
const to = Math.max(start, end);
|
|
532
|
-
for (let index = from; index <= to; index++) {
|
|
533
|
-
const keys = this.commentLineKeys.get(index);
|
|
534
|
-
if (keys) {
|
|
535
|
-
keys.push(key);
|
|
536
|
-
} else {
|
|
537
|
-
this.commentLineKeys.set(index, [key]);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
834
|
+
this.commentLineKeys = buildCommentLineKeys(
|
|
835
|
+
this.comments,
|
|
836
|
+
this.lineIndexById,
|
|
837
|
+
);
|
|
542
838
|
this.commentLineKeysRevision = this.commentsRevision;
|
|
543
839
|
}
|
|
544
840
|
|
|
545
|
-
private buildGlobalComment(text: string): ReviewComment {
|
|
546
|
-
return {
|
|
547
|
-
id: GLOBAL_COMMENT_KEY,
|
|
548
|
-
filePath: "Overall diff",
|
|
549
|
-
text,
|
|
550
|
-
global: true,
|
|
551
|
-
startLineId: GLOBAL_COMMENT_KEY,
|
|
552
|
-
endLineId: GLOBAL_COMMENT_KEY,
|
|
553
|
-
lineText: "",
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
private buildCommentFromSelection(
|
|
558
|
-
selection: SelectionBounds,
|
|
559
|
-
text: string,
|
|
560
|
-
): ReviewComment {
|
|
561
|
-
const startLine = this.lines[selection.start]!;
|
|
562
|
-
const endLine = this.lines[selection.end]!;
|
|
563
|
-
const excerpt = this.lines
|
|
564
|
-
.slice(selection.start, selection.end + 1)
|
|
565
|
-
.map((line) => line.text)
|
|
566
|
-
.join("\n");
|
|
567
|
-
return {
|
|
568
|
-
id: this.getSelectionKey(selection.start, selection.end),
|
|
569
|
-
filePath: startLine.filePath ?? endLine.filePath ?? "(unknown file)",
|
|
570
|
-
text,
|
|
571
|
-
startLineId: startLine.id,
|
|
572
|
-
endLineId: endLine.id,
|
|
573
|
-
startOldLineNumber: startLine.oldLineNumber,
|
|
574
|
-
startNewLineNumber: startLine.newLineNumber,
|
|
575
|
-
endOldLineNumber: endLine.oldLineNumber,
|
|
576
|
-
endNewLineNumber: endLine.newLineNumber,
|
|
577
|
-
lineText: excerpt,
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
841
|
private getPositionText(selectedLine?: ReviewLine): string {
|
|
582
842
|
const position = `${Math.min(this.selected + 1, this.lines.length)}/${this.lines.length}`;
|
|
583
843
|
return selectedLine?.filePath
|
|
@@ -621,10 +881,12 @@ export class ReviewComponent {
|
|
|
621
881
|
|
|
622
882
|
private startGlobalEditMode(): void {
|
|
623
883
|
const existing = this.comments.get(GLOBAL_COMMENT_KEY);
|
|
624
|
-
this.
|
|
884
|
+
this.inlineAnnotationsVisible = true;
|
|
885
|
+
this.hideInlineExplanation();
|
|
625
886
|
this.editMode = true;
|
|
626
887
|
this.editingCommentKey = GLOBAL_COMMENT_KEY;
|
|
627
888
|
this.editor.setText(existing?.text ?? "");
|
|
889
|
+
this.invalidateAnnotatedRows();
|
|
628
890
|
this.tui.requestRender(true);
|
|
629
891
|
}
|
|
630
892
|
|
|
@@ -637,13 +899,15 @@ export class ReviewComponent {
|
|
|
637
899
|
if (startLine.filePath !== endLine.filePath) return;
|
|
638
900
|
|
|
639
901
|
const existing = this.getCommentForSelection(selection);
|
|
640
|
-
this.
|
|
902
|
+
this.inlineAnnotationsVisible = true;
|
|
903
|
+
this.hideInlineExplanation();
|
|
641
904
|
this.editMode = true;
|
|
642
905
|
this.editingCommentKey = this.getSelectionKey(
|
|
643
906
|
selection.start,
|
|
644
907
|
selection.end,
|
|
645
908
|
);
|
|
646
909
|
this.editor.setText(existing?.text ?? "");
|
|
910
|
+
this.invalidateAnnotatedRows();
|
|
647
911
|
this.tui.requestRender(true);
|
|
648
912
|
}
|
|
649
913
|
|
|
@@ -651,78 +915,25 @@ export class ReviewComponent {
|
|
|
651
915
|
this.editMode = false;
|
|
652
916
|
this.editingCommentKey = undefined;
|
|
653
917
|
this.editor.setText("");
|
|
918
|
+
this.invalidateAnnotatedRows();
|
|
654
919
|
this.tui.requestRender(true);
|
|
655
920
|
}
|
|
656
921
|
|
|
657
922
|
private getSplitDiffRows(): SplitDiffRow[] {
|
|
658
923
|
if (this.splitRows) return this.splitRows;
|
|
659
924
|
|
|
660
|
-
const rows
|
|
661
|
-
const rowByLineIndex: number[] = [];
|
|
662
|
-
let index = 0;
|
|
663
|
-
|
|
664
|
-
const pushRow = (row: SplitDiffRow) => {
|
|
665
|
-
const displayRow = rows.length;
|
|
666
|
-
rows.push(row);
|
|
667
|
-
if (row.kind === "full") {
|
|
668
|
-
rowByLineIndex[row.cell.index] = displayRow;
|
|
669
|
-
} else {
|
|
670
|
-
if (row.left) rowByLineIndex[row.left.index] = displayRow;
|
|
671
|
-
if (row.right) rowByLineIndex[row.right.index] = displayRow;
|
|
672
|
-
}
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
while (index < this.lines.length) {
|
|
676
|
-
const line = this.lines[index]!;
|
|
677
|
-
|
|
678
|
-
if (line.kind === "remove" || line.kind === "add") {
|
|
679
|
-
const removals: SplitDiffCell[] = [];
|
|
680
|
-
const additions: SplitDiffCell[] = [];
|
|
681
|
-
|
|
682
|
-
while (this.lines[index]?.kind === "remove") {
|
|
683
|
-
removals.push({ line: this.lines[index]!, index });
|
|
684
|
-
index++;
|
|
685
|
-
}
|
|
686
|
-
while (this.lines[index]?.kind === "add") {
|
|
687
|
-
additions.push({ line: this.lines[index]!, index });
|
|
688
|
-
index++;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const count = Math.max(removals.length, additions.length);
|
|
692
|
-
for (let offset = 0; offset < count; offset++) {
|
|
693
|
-
pushRow({
|
|
694
|
-
kind: "split",
|
|
695
|
-
left: removals[offset],
|
|
696
|
-
right: additions[offset],
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (line.kind === "context") {
|
|
703
|
-
const cell = { line, index };
|
|
704
|
-
pushRow({ kind: "split", left: cell, right: cell });
|
|
705
|
-
} else {
|
|
706
|
-
pushRow({ kind: "full", cell: { line, index } });
|
|
707
|
-
}
|
|
708
|
-
index++;
|
|
709
|
-
}
|
|
710
|
-
|
|
925
|
+
const { rows } = buildSplitDiffRows(this.lines);
|
|
711
926
|
this.splitRows = rows;
|
|
712
|
-
this.splitRowByLineIndex = rowByLineIndex;
|
|
713
927
|
return rows;
|
|
714
928
|
}
|
|
715
929
|
|
|
716
|
-
private getSelectedDisplayRow(): number {
|
|
717
|
-
|
|
718
|
-
this.
|
|
719
|
-
return this.splitRowByLineIndex?.[this.selected] ?? 0;
|
|
930
|
+
private getSelectedDisplayRow(width: number): number {
|
|
931
|
+
this.getAnnotatedRows(width);
|
|
932
|
+
return this.annotatedRowByLineIndex?.[this.selected] ?? 0;
|
|
720
933
|
}
|
|
721
934
|
|
|
722
|
-
private getDisplayRowCount(): number {
|
|
723
|
-
return this.
|
|
724
|
-
? this.lines.length
|
|
725
|
-
: this.getSplitDiffRows().length;
|
|
935
|
+
private getDisplayRowCount(width: number): number {
|
|
936
|
+
return this.getAnnotatedRows(width).length;
|
|
726
937
|
}
|
|
727
938
|
|
|
728
939
|
private renderSplitDiffCell(
|
|
@@ -732,7 +943,7 @@ export class ReviewComponent {
|
|
|
732
943
|
): string {
|
|
733
944
|
const { line, index } = cell;
|
|
734
945
|
const hasComment = this.getCommentKeysForLine(index).length > 0;
|
|
735
|
-
const commentMark = hasComment ? this.theme.fg("
|
|
946
|
+
const commentMark = hasComment ? this.theme.fg("borderAccent", "│") : " ";
|
|
736
947
|
const lineNumber =
|
|
737
948
|
side === "left" ? line.oldLineNumber : line.newLineNumber;
|
|
738
949
|
const prefix = `${commentMark} ${lineNumberCell(lineNumber)} `;
|
|
@@ -748,19 +959,11 @@ export class ReviewComponent {
|
|
|
748
959
|
return this.applyDiffBackground(line, styled, width);
|
|
749
960
|
}
|
|
750
961
|
|
|
751
|
-
private ensureScroll(viewportHeight: number): void {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
this.scrollTop = selectedRow;
|
|
757
|
-
}
|
|
758
|
-
if (selectedRow >= this.scrollTop + viewportHeight) {
|
|
759
|
-
this.scrollTop = selectedRow - viewportHeight + 1;
|
|
760
|
-
}
|
|
761
|
-
this.scrollTop = Math.max(
|
|
762
|
-
0,
|
|
763
|
-
Math.min(this.scrollTop, Math.max(0, rowCount - viewportHeight)),
|
|
962
|
+
private ensureScroll(viewportHeight: number, width: number): void {
|
|
963
|
+
this.navigation.ensureScroll(
|
|
964
|
+
viewportHeight,
|
|
965
|
+
this.getSelectedDisplayRow(width),
|
|
966
|
+
this.getDisplayRowCount(width),
|
|
764
967
|
);
|
|
765
968
|
}
|
|
766
969
|
|
|
@@ -829,7 +1032,7 @@ export class ReviewComponent {
|
|
|
829
1032
|
selection?: SelectionBounds,
|
|
830
1033
|
): string {
|
|
831
1034
|
const hasComment = this.getCommentKeysForLine(index).length > 0;
|
|
832
|
-
const commentMark = hasComment ? this.theme.fg("
|
|
1035
|
+
const commentMark = hasComment ? this.theme.fg("borderAccent", "│") : " ";
|
|
833
1036
|
const numbers = `${lineNumberCell(line.oldLineNumber)} ${lineNumberCell(line.newLineNumber)}`;
|
|
834
1037
|
const prefix = `${commentMark} ${numbers} `;
|
|
835
1038
|
let styled = this.renderDiffRowContent(line, prefix);
|
|
@@ -843,383 +1046,11 @@ export class ReviewComponent {
|
|
|
843
1046
|
return this.applyDiffBackground(line, styled, width);
|
|
844
1047
|
}
|
|
845
1048
|
|
|
846
|
-
private renderRightPane(
|
|
847
|
-
width: number,
|
|
848
|
-
height: number,
|
|
849
|
-
selectedLine?: ReviewLine,
|
|
850
|
-
): string[] {
|
|
851
|
-
return this.rightPaneMode === "explanation"
|
|
852
|
-
? this.renderExplanationPane(width, height, selectedLine)
|
|
853
|
-
: this.renderCommentsPane(width, height, selectedLine);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
private renderCommentsPane(
|
|
857
|
-
width: number,
|
|
858
|
-
height: number,
|
|
859
|
-
selectedLine?: ReviewLine,
|
|
860
|
-
): string[] {
|
|
861
|
-
const lines: string[] = [];
|
|
862
|
-
const title = this.theme.fg("accent", this.theme.bold("Comments"));
|
|
863
|
-
const selection = this.getActiveCommentSelection();
|
|
864
|
-
const currentComment = this.getCommentForSelection(selection);
|
|
865
|
-
|
|
866
|
-
lines.push(truncateToWidth(title, width));
|
|
867
|
-
lines.push(
|
|
868
|
-
truncateToWidth(
|
|
869
|
-
this.theme.fg(
|
|
870
|
-
"dim",
|
|
871
|
-
selection
|
|
872
|
-
? this.getFooterText(selectedLine)
|
|
873
|
-
: selectedLine
|
|
874
|
-
? formatLocation(selectedLine)
|
|
875
|
-
: "No selection",
|
|
876
|
-
),
|
|
877
|
-
width,
|
|
878
|
-
),
|
|
879
|
-
);
|
|
880
|
-
lines.push("");
|
|
881
|
-
|
|
882
|
-
if (this.editMode && this.editingCommentKey === GLOBAL_COMMENT_KEY) {
|
|
883
|
-
lines[1] = truncateToWidth(
|
|
884
|
-
this.theme.fg("dim", "Overall diff comment"),
|
|
885
|
-
width,
|
|
886
|
-
);
|
|
887
|
-
lines.push(
|
|
888
|
-
...wrapTextWithAnsi(
|
|
889
|
-
this.theme.fg(
|
|
890
|
-
"dim",
|
|
891
|
-
"Editing overall diff comment. Enter saves. Esc or Ctrl+C cancels.",
|
|
892
|
-
),
|
|
893
|
-
width,
|
|
894
|
-
),
|
|
895
|
-
);
|
|
896
|
-
lines.push("");
|
|
897
|
-
for (const line of this.editor.render(Math.max(10, width))) {
|
|
898
|
-
lines.push(truncateToWidth(line, width));
|
|
899
|
-
}
|
|
900
|
-
return lines.slice(0, height);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const globalComment = this.comments.get(GLOBAL_COMMENT_KEY);
|
|
904
|
-
if (globalComment) {
|
|
905
|
-
lines.push(
|
|
906
|
-
truncateToWidth(
|
|
907
|
-
this.theme.fg("accent", this.theme.bold("Overall diff comment")),
|
|
908
|
-
width,
|
|
909
|
-
),
|
|
910
|
-
);
|
|
911
|
-
lines.push(
|
|
912
|
-
...wrapTextWithAnsi(this.theme.fg("text", globalComment.text), width),
|
|
913
|
-
);
|
|
914
|
-
lines.push(...wrapTextWithAnsi(this.theme.fg("dim", "C edits"), width));
|
|
915
|
-
lines.push("");
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
if (!selectedLine) {
|
|
919
|
-
lines.push(
|
|
920
|
-
...wrapTextWithAnsi(
|
|
921
|
-
this.theme.fg("muted", "No diff lines available."),
|
|
922
|
-
width,
|
|
923
|
-
),
|
|
924
|
-
);
|
|
925
|
-
return lines.slice(0, height);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if (!selection) {
|
|
929
|
-
lines.push(
|
|
930
|
-
...wrapTextWithAnsi(
|
|
931
|
-
this.theme.fg(
|
|
932
|
-
"muted",
|
|
933
|
-
"Move to a diff line and press c to add a comment, or press C for an overall diff comment.",
|
|
934
|
-
),
|
|
935
|
-
width,
|
|
936
|
-
),
|
|
937
|
-
);
|
|
938
|
-
return lines.slice(0, height);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
if (this.editMode && currentComment?.id === this.editingCommentKey) {
|
|
942
|
-
lines.push(
|
|
943
|
-
...wrapTextWithAnsi(
|
|
944
|
-
this.theme.fg(
|
|
945
|
-
"dim",
|
|
946
|
-
"Editing comment. Enter saves. Esc or Ctrl+C cancels.",
|
|
947
|
-
),
|
|
948
|
-
width,
|
|
949
|
-
),
|
|
950
|
-
);
|
|
951
|
-
lines.push("");
|
|
952
|
-
for (const line of this.editor.render(Math.max(10, width))) {
|
|
953
|
-
lines.push(truncateToWidth(line, width));
|
|
954
|
-
}
|
|
955
|
-
} else if (this.editMode && this.editingCommentKey) {
|
|
956
|
-
lines.push(
|
|
957
|
-
...wrapTextWithAnsi(
|
|
958
|
-
this.theme.fg(
|
|
959
|
-
"dim",
|
|
960
|
-
"Editing comment. Enter saves. Esc or Ctrl+C cancels.",
|
|
961
|
-
),
|
|
962
|
-
width,
|
|
963
|
-
),
|
|
964
|
-
);
|
|
965
|
-
lines.push("");
|
|
966
|
-
for (const line of this.editor.render(Math.max(10, width))) {
|
|
967
|
-
lines.push(truncateToWidth(line, width));
|
|
968
|
-
}
|
|
969
|
-
} else if (currentComment) {
|
|
970
|
-
lines.push(
|
|
971
|
-
...wrapTextWithAnsi(this.theme.fg("text", currentComment.text), width),
|
|
972
|
-
);
|
|
973
|
-
lines.push("");
|
|
974
|
-
lines.push(
|
|
975
|
-
...wrapTextWithAnsi(
|
|
976
|
-
this.theme.fg("dim", "x deletes this comment • c edits"),
|
|
977
|
-
width,
|
|
978
|
-
),
|
|
979
|
-
);
|
|
980
|
-
} else {
|
|
981
|
-
lines.push(
|
|
982
|
-
...wrapTextWithAnsi(
|
|
983
|
-
this.theme.fg(
|
|
984
|
-
"muted",
|
|
985
|
-
this.hasSelection()
|
|
986
|
-
? "No comment on this range."
|
|
987
|
-
: "No comment on this line.",
|
|
988
|
-
),
|
|
989
|
-
width,
|
|
990
|
-
),
|
|
991
|
-
);
|
|
992
|
-
lines.push("");
|
|
993
|
-
lines.push(
|
|
994
|
-
...wrapTextWithAnsi(
|
|
995
|
-
this.theme.fg(
|
|
996
|
-
"dim",
|
|
997
|
-
this.hasSelection()
|
|
998
|
-
? "Press c to add a range comment, or C for an overall diff comment."
|
|
999
|
-
: "Press c to add one. Use J/K to extend a range. Press C for an overall diff comment.",
|
|
1000
|
-
),
|
|
1001
|
-
width,
|
|
1002
|
-
),
|
|
1003
|
-
);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
lines.push("");
|
|
1007
|
-
lines.push(
|
|
1008
|
-
truncateToWidth(
|
|
1009
|
-
this.theme.fg("accent", this.theme.bold("Excerpt")),
|
|
1010
|
-
width,
|
|
1011
|
-
),
|
|
1012
|
-
);
|
|
1013
|
-
const excerpt = this.lines
|
|
1014
|
-
.slice(selection.start, selection.end + 1)
|
|
1015
|
-
.map((line) => line.text)
|
|
1016
|
-
.join("\n");
|
|
1017
|
-
lines.push(
|
|
1018
|
-
...wrapTextWithAnsi(
|
|
1019
|
-
this.theme.fg("toolDiffContext", excerpt || "(blank line)"),
|
|
1020
|
-
width,
|
|
1021
|
-
),
|
|
1022
|
-
);
|
|
1023
|
-
return lines.slice(0, height);
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
private renderExplanationPane(
|
|
1027
|
-
width: number,
|
|
1028
|
-
height: number,
|
|
1029
|
-
selectedLine?: ReviewLine,
|
|
1030
|
-
): string[] {
|
|
1031
|
-
const lines: string[] = [];
|
|
1032
|
-
const title = this.theme.fg("accent", this.theme.bold("Explanation"));
|
|
1033
|
-
const scope = this.getCurrentHunkScope();
|
|
1034
|
-
|
|
1035
|
-
lines.push(truncateToWidth(title, width));
|
|
1036
|
-
lines.push(
|
|
1037
|
-
truncateToWidth(
|
|
1038
|
-
this.theme.fg(
|
|
1039
|
-
"dim",
|
|
1040
|
-
scope?.title ??
|
|
1041
|
-
(selectedLine ? formatLocation(selectedLine) : "No selection"),
|
|
1042
|
-
),
|
|
1043
|
-
width,
|
|
1044
|
-
),
|
|
1045
|
-
);
|
|
1046
|
-
lines.push("");
|
|
1047
|
-
|
|
1048
|
-
if (!this.explainer) {
|
|
1049
|
-
lines.push(
|
|
1050
|
-
...wrapTextWithAnsi(
|
|
1051
|
-
this.theme.fg("warning", "Diff explanations are unavailable."),
|
|
1052
|
-
width,
|
|
1053
|
-
),
|
|
1054
|
-
);
|
|
1055
|
-
return lines.slice(0, height);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (!scope) {
|
|
1059
|
-
lines.push(
|
|
1060
|
-
...wrapTextWithAnsi(
|
|
1061
|
-
this.theme.fg(
|
|
1062
|
-
"muted",
|
|
1063
|
-
"Move to a changed hunk and press ? to generate an explanation.",
|
|
1064
|
-
),
|
|
1065
|
-
width,
|
|
1066
|
-
),
|
|
1067
|
-
);
|
|
1068
|
-
return lines.slice(0, height);
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
const explanation = this.explanations.get(scope.key);
|
|
1072
|
-
if (!explanation) {
|
|
1073
|
-
lines.push(
|
|
1074
|
-
...wrapTextWithAnsi(
|
|
1075
|
-
this.theme.fg(
|
|
1076
|
-
"muted",
|
|
1077
|
-
"No explanation generated yet. Press ? again after returning to comments to generate this hunk.",
|
|
1078
|
-
),
|
|
1079
|
-
width,
|
|
1080
|
-
),
|
|
1081
|
-
);
|
|
1082
|
-
} else if (explanation.status === "loading") {
|
|
1083
|
-
const spinner = this.getLoadingFrame();
|
|
1084
|
-
lines.push(
|
|
1085
|
-
truncateToWidth(
|
|
1086
|
-
this.theme.fg("accent", `${spinner} Generating explanation...`),
|
|
1087
|
-
width,
|
|
1088
|
-
),
|
|
1089
|
-
);
|
|
1090
|
-
if (explanation.text.trim()) {
|
|
1091
|
-
lines.push("");
|
|
1092
|
-
lines.push(
|
|
1093
|
-
...wrapTextWithAnsi(this.theme.fg("text", explanation.text), width),
|
|
1094
|
-
);
|
|
1095
|
-
}
|
|
1096
|
-
} else if (explanation.status === "error") {
|
|
1097
|
-
lines.push(
|
|
1098
|
-
...wrapTextWithAnsi(
|
|
1099
|
-
this.theme.fg(
|
|
1100
|
-
"warning",
|
|
1101
|
-
`Unable to explain diff: ${explanation.message}`,
|
|
1102
|
-
),
|
|
1103
|
-
width,
|
|
1104
|
-
),
|
|
1105
|
-
);
|
|
1106
|
-
} else {
|
|
1107
|
-
lines.push(
|
|
1108
|
-
...wrapTextWithAnsi(this.theme.fg("text", explanation.text), width),
|
|
1109
|
-
);
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
lines.push("");
|
|
1113
|
-
lines.push(...wrapTextWithAnsi(this.theme.fg("dim", "? comments"), width));
|
|
1114
|
-
return lines.slice(0, height);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
1049
|
private ensureCurrentExplanation(): void {
|
|
1118
|
-
|
|
1119
|
-
if (!scope || !this.explainer) return;
|
|
1120
|
-
if (this.explanations.has(scope.key)) return;
|
|
1121
|
-
|
|
1122
|
-
this.explanationAbort?.abort();
|
|
1123
|
-
const controller = new AbortController();
|
|
1124
|
-
this.explanationAbort = controller;
|
|
1125
|
-
const requestId = ++this.explanationRequestId;
|
|
1126
|
-
let text = "";
|
|
1127
|
-
|
|
1128
|
-
this.explanations.set(scope.key, { status: "loading", text });
|
|
1129
|
-
this.startLoadingTimer();
|
|
1130
|
-
|
|
1131
|
-
void this.explainer
|
|
1132
|
-
.explain(scope, {
|
|
1133
|
-
signal: controller.signal,
|
|
1134
|
-
onDelta: (delta) => {
|
|
1135
|
-
if (requestId !== this.explanationRequestId) return;
|
|
1136
|
-
text += delta;
|
|
1137
|
-
this.explanations.set(scope.key, { status: "loading", text });
|
|
1138
|
-
this.tui.requestRender();
|
|
1139
|
-
},
|
|
1140
|
-
})
|
|
1141
|
-
.then((finalText) => {
|
|
1142
|
-
if (requestId !== this.explanationRequestId) return;
|
|
1143
|
-
this.explanations.set(scope.key, {
|
|
1144
|
-
status: "ready",
|
|
1145
|
-
text: finalText.trim() || text.trim() || "No explanation returned.",
|
|
1146
|
-
});
|
|
1147
|
-
})
|
|
1148
|
-
.catch((error) => {
|
|
1149
|
-
if (requestId !== this.explanationRequestId) return;
|
|
1150
|
-
if (controller.signal.aborted) return;
|
|
1151
|
-
this.explanations.set(scope.key, {
|
|
1152
|
-
status: "error",
|
|
1153
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1154
|
-
});
|
|
1155
|
-
})
|
|
1156
|
-
.finally(() => {
|
|
1157
|
-
if (requestId !== this.explanationRequestId) return;
|
|
1158
|
-
this.stopLoadingTimerIfIdle();
|
|
1159
|
-
this.tui.requestRender();
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
this.tui.requestRender();
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
private getCurrentHunkScope(): ExplanationScope | undefined {
|
|
1166
|
-
const selectedLine = this.lines[this.selected];
|
|
1167
|
-
if (!selectedLine?.filePath || !selectedLine.hunkLabel) return undefined;
|
|
1168
|
-
|
|
1169
|
-
let start = this.selected;
|
|
1170
|
-
while (
|
|
1171
|
-
start > 0 &&
|
|
1172
|
-
this.lines[start - 1]?.filePath === selectedLine.filePath &&
|
|
1173
|
-
this.lines[start - 1]?.hunkLabel === selectedLine.hunkLabel
|
|
1174
|
-
) {
|
|
1175
|
-
start--;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
let end = this.selected;
|
|
1179
|
-
while (
|
|
1180
|
-
end + 1 < this.lines.length &&
|
|
1181
|
-
this.lines[end + 1]?.filePath === selectedLine.filePath &&
|
|
1182
|
-
this.lines[end + 1]?.hunkLabel === selectedLine.hunkLabel
|
|
1183
|
-
) {
|
|
1184
|
-
end++;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const diffText = this.lines
|
|
1188
|
-
.slice(start, end + 1)
|
|
1189
|
-
.map((line) => line.text)
|
|
1190
|
-
.join("\n");
|
|
1191
|
-
return {
|
|
1192
|
-
key: `hunk:${selectedLine.filePath}:${selectedLine.hunkLabel}:${start}:${end}`,
|
|
1193
|
-
kind: "hunk",
|
|
1194
|
-
title: `${selectedLine.filePath} ${selectedLine.hunkLabel}`,
|
|
1195
|
-
filePath: selectedLine.filePath,
|
|
1196
|
-
diffText,
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
private getLoadingFrame(): string {
|
|
1201
|
-
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
1202
|
-
return frames[this.loadingFrame % frames.length] ?? "⠋";
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
private startLoadingTimer(): void {
|
|
1206
|
-
if (this.loadingTimer) return;
|
|
1207
|
-
this.loadingTimer = setInterval(() => {
|
|
1208
|
-
this.loadingFrame++;
|
|
1209
|
-
this.tui.requestRender();
|
|
1210
|
-
}, 120);
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
private stopLoadingTimerIfIdle(): void {
|
|
1214
|
-
const hasLoading = [...this.explanations.values()].some(
|
|
1215
|
-
(explanation) => explanation.status === "loading",
|
|
1216
|
-
);
|
|
1217
|
-
if (!hasLoading) this.stopLoadingTimer();
|
|
1050
|
+
this.explanationController.ensure(this.getCurrentHunkScope());
|
|
1218
1051
|
}
|
|
1219
1052
|
|
|
1220
|
-
private
|
|
1221
|
-
|
|
1222
|
-
clearInterval(this.loadingTimer);
|
|
1223
|
-
this.loadingTimer = undefined;
|
|
1053
|
+
private getCurrentHunkScope() {
|
|
1054
|
+
return getCurrentHunkScope(this.lines, this.selected);
|
|
1224
1055
|
}
|
|
1225
1056
|
}
|