pi-diff-review 0.1.24 → 0.1.26
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 +23 -7
- package/package.json +1 -1
- package/src/diff/source.ts +59 -5
- package/src/diff/turn-based-overlay.ts +93 -0
- package/src/index.ts +34 -0
- package/src/review/cache.ts +72 -1
- package/src/review/component.ts +273 -26
- package/src/review/navigation.ts +7 -0
- package/src/review/types.ts +7 -0
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Embedded code reviews and
|
|
6
|
-
|
|
7
|
-
<img width="
|
|
1
|
+
<div align="center">
|
|
2
|
+
<div>
|
|
3
|
+
<img height="96" alt="pi-diff-review" src="https://github.com/user-attachments/assets/7b97e6a2-9dc3-49f8-b932-f8a90a59bfcf" />
|
|
4
|
+
</div>
|
|
5
|
+
<b><i>Embedded code reviews and explanations directly within <a href="https://pi.dev">pi</a>.</i></b>
|
|
6
|
+
<div>
|
|
7
|
+
<img width="728" alt="image" src="https://github.com/user-attachments/assets/6bb72e25-369e-446b-888d-1561005c427e" />
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
8
10
|
|
|
9
11
|
## Install
|
|
10
12
|
|
|
@@ -57,6 +59,18 @@ Review a single file by using a git pathspec after `--`. Pi path autocomplete wo
|
|
|
57
59
|
|
|
58
60
|
`/diff <git-diff-args>` is passed through to `git diff`, so these examples are equivalent to running `git diff`, `git diff --cached`, and `git diff main...HEAD` locally before opening the review UI.
|
|
59
61
|
|
|
62
|
+
Experimental: track reviewed turns while keeping the full overall diff visible:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
/diff --turn-based
|
|
66
|
+
/diff --cached --turn-based
|
|
67
|
+
/diff main...HEAD --turn-based -- @src/index.ts
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Press `M` in a turn-based review to toggle the current hunk as reviewed. Later
|
|
71
|
+
runs show the requested overall diff and render reviewed changed lines with a
|
|
72
|
+
muted blue overlay.
|
|
73
|
+
|
|
60
74
|
Open one or more files or folders with `/view`:
|
|
61
75
|
|
|
62
76
|
```text
|
|
@@ -79,6 +93,7 @@ Open one or more files or folders with `/view`:
|
|
|
79
93
|
- `/diff --cached` reviews staged changes
|
|
80
94
|
- `/diff main...HEAD` reviews changes on the current branch relative to `main`
|
|
81
95
|
- `/diff <git-diff-args>` passes arguments through to `git diff`
|
|
96
|
+
- `/diff --turn-based` enables experimental reviewed-turn overlays with `M`
|
|
82
97
|
- `h` toggles the command help modal
|
|
83
98
|
- `j/k` or arrow keys to move
|
|
84
99
|
- `g/G` to jump to the top or bottom of the diff
|
|
@@ -88,6 +103,7 @@ Open one or more files or folders with `/view`:
|
|
|
88
103
|
- `ctrl-u` / `ctrl-d` to move up/down by half a page
|
|
89
104
|
- `s` toggles inline comments/explanations
|
|
90
105
|
- `v` toggles the diff between unified and side-by-side split rendering
|
|
106
|
+
- `w` toggles line wrap for long diff lines
|
|
91
107
|
- `?` toggles an AI-generated explanation for the current hunk
|
|
92
108
|
- `/` searches visible diff text, highlights matches, and `n/N` moves between them while a search is active
|
|
93
109
|
- `J/K` to extend a highlighted selection into a comment range
|
package/package.json
CHANGED
package/src/diff/source.ts
CHANGED
|
@@ -12,23 +12,77 @@ export function parseDiffSource(args: string): DiffSource {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const gitArgs =
|
|
15
|
+
const { gitArgs, turnBased } = parseDiffArgs(trimmed);
|
|
16
|
+
if (!turnBased) {
|
|
17
|
+
return {
|
|
18
|
+
label: `git diff ${trimmed}`,
|
|
19
|
+
promptLabel: `\`git diff ${trimmed}\``,
|
|
20
|
+
args: gitArgs,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const gitDiffLabel =
|
|
25
|
+
gitArgs.length > 0 ? `git diff ${gitArgs.join(" ")}` : "unstaged git diff";
|
|
26
|
+
const promptLabel =
|
|
27
|
+
gitArgs.length > 0
|
|
28
|
+
? `\`git diff ${gitArgs.join(" ")}\``
|
|
29
|
+
: "the current unstaged git diff";
|
|
30
|
+
|
|
16
31
|
return {
|
|
17
|
-
label:
|
|
18
|
-
promptLabel
|
|
32
|
+
label: `${gitDiffLabel} with turn-based review overlay`,
|
|
33
|
+
promptLabel,
|
|
19
34
|
args: gitArgs,
|
|
35
|
+
turnBased: true,
|
|
20
36
|
};
|
|
21
37
|
}
|
|
22
38
|
|
|
23
|
-
function
|
|
39
|
+
function parseDiffArgs(input: string): {
|
|
40
|
+
gitArgs: string[];
|
|
41
|
+
turnBased: boolean;
|
|
42
|
+
} {
|
|
24
43
|
try {
|
|
25
|
-
|
|
44
|
+
const tokens = tokenizeShellArgs(input);
|
|
45
|
+
const { args, turnBased } = extractDiffReviewFlags(tokens);
|
|
46
|
+
return {
|
|
47
|
+
gitArgs: normalizeDiffPathspecs(args),
|
|
48
|
+
turnBased,
|
|
49
|
+
};
|
|
26
50
|
} catch (error) {
|
|
27
51
|
const message = error instanceof Error ? error.message : String(error);
|
|
28
52
|
throw new Error(message.replace(/in arguments$/, "in git diff args"));
|
|
29
53
|
}
|
|
30
54
|
}
|
|
31
55
|
|
|
56
|
+
function extractDiffReviewFlags(tokens: string[]): {
|
|
57
|
+
args: string[];
|
|
58
|
+
turnBased: boolean;
|
|
59
|
+
} {
|
|
60
|
+
const args: string[] = [];
|
|
61
|
+
let turnBased = false;
|
|
62
|
+
let passthrough = false;
|
|
63
|
+
|
|
64
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
65
|
+
const token = tokens[index]!;
|
|
66
|
+
if (token === "--") {
|
|
67
|
+
passthrough = true;
|
|
68
|
+
args.push(token);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!passthrough && token === "--turn-based") {
|
|
73
|
+
if (turnBased) {
|
|
74
|
+
throw new Error("--turn-based can only be provided once");
|
|
75
|
+
}
|
|
76
|
+
turnBased = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
args.push(token);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { args, turnBased };
|
|
84
|
+
}
|
|
85
|
+
|
|
32
86
|
function normalizeDiffPathspecs(args: string[]): string[] {
|
|
33
87
|
const separatorIndex = args.indexOf("--");
|
|
34
88
|
if (separatorIndex < 0) return args;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ReviewLine, ReviewSnapshotLine } from "../review/types.ts";
|
|
2
|
+
|
|
3
|
+
export function applyReviewedOverlay(
|
|
4
|
+
targetLines: ReviewLine[],
|
|
5
|
+
reviewedLines: ReviewSnapshotLine[],
|
|
6
|
+
): number {
|
|
7
|
+
const reviewedKeys = new Set(
|
|
8
|
+
reviewedLines.flatMap((line) => getChangedLineKeys(line)),
|
|
9
|
+
);
|
|
10
|
+
let marked = 0;
|
|
11
|
+
|
|
12
|
+
for (const line of targetLines) {
|
|
13
|
+
if (getChangedLineKeys(line).some((key) => reviewedKeys.has(key))) {
|
|
14
|
+
line.reviewedOverlay = true;
|
|
15
|
+
marked++;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return marked;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function markChangedLinesReviewed(lines: ReviewLine[]): number {
|
|
23
|
+
let marked = 0;
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (getChangedLineKeys(line).length === 0) continue;
|
|
27
|
+
if (!line.reviewedOverlay) marked++;
|
|
28
|
+
line.reviewedOverlay = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return marked;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function clearReviewedOverlay(lines: ReviewLine[]): number {
|
|
35
|
+
let cleared = 0;
|
|
36
|
+
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (!line.reviewedOverlay) continue;
|
|
39
|
+
line.reviewedOverlay = false;
|
|
40
|
+
cleared++;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return cleared;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function areAllChangedLinesReviewed(lines: ReviewLine[]): boolean {
|
|
47
|
+
const changedLines = lines.filter(
|
|
48
|
+
(line) => getChangedLineKeys(line).length > 0,
|
|
49
|
+
);
|
|
50
|
+
return (
|
|
51
|
+
changedLines.length > 0 &&
|
|
52
|
+
changedLines.every((line) => line.reviewedOverlay)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getReviewedLines(lines: ReviewLine[]): ReviewLine[] {
|
|
57
|
+
return lines.filter(
|
|
58
|
+
(line) => line.reviewedOverlay && getChangedLineKeys(line).length > 0,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getChangedLineKeys(line: ReviewLine | ReviewSnapshotLine): string[] {
|
|
63
|
+
if (!line.filePath) return [];
|
|
64
|
+
if (line.kind === "add" && line.newLineNumber != null) {
|
|
65
|
+
return [
|
|
66
|
+
[
|
|
67
|
+
line.filePath,
|
|
68
|
+
line.kind,
|
|
69
|
+
String(line.newLineNumber),
|
|
70
|
+
getComparableText(line),
|
|
71
|
+
].join("\0"),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (line.kind === "remove" && line.oldLineNumber != null) {
|
|
76
|
+
return [
|
|
77
|
+
[
|
|
78
|
+
line.filePath,
|
|
79
|
+
line.kind,
|
|
80
|
+
String(line.oldLineNumber),
|
|
81
|
+
getComparableText(line),
|
|
82
|
+
].join("\0"),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getComparableText(line: ReviewLine | ReviewSnapshotLine): string {
|
|
90
|
+
return line.kind === "add" || line.kind === "remove"
|
|
91
|
+
? line.text.slice(1)
|
|
92
|
+
: line.text;
|
|
93
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,11 +5,14 @@ import type {
|
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { getDiff, parseDiffSource } from "./diff/source.ts";
|
|
7
7
|
import { parseDiff } from "./diff/parser.ts";
|
|
8
|
+
import { applyReviewedOverlay } from "./diff/turn-based-overlay.ts";
|
|
8
9
|
import { PiModelDiffExplainer } from "./explanation/explainer.ts";
|
|
9
10
|
import {
|
|
11
|
+
getLastReviewSnapshot,
|
|
10
12
|
getCachedAsk,
|
|
11
13
|
getCachedComments,
|
|
12
14
|
getCachedExplanations,
|
|
15
|
+
persistLastReviewSnapshot,
|
|
13
16
|
persistCachedAsk,
|
|
14
17
|
persistCachedComments,
|
|
15
18
|
persistCachedExplanations,
|
|
@@ -30,6 +33,10 @@ function getReviewCacheKey(cwd: string, label: string, text: string): string {
|
|
|
30
33
|
return `${cwd}\0${label}\0${hash}`;
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
function getReviewScopeKey(cwd: string, args: string[]): string {
|
|
37
|
+
return `${cwd}\0diff\0${JSON.stringify(args)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
34
41
|
pi.registerCommand("diff", {
|
|
35
42
|
description: "Review a git diff in a custom TUI (/diff [git diff args])",
|
|
@@ -51,11 +58,34 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
const reviewLines = parseDiff(diffText);
|
|
61
|
+
const turnBasedScopeKey = source.turnBased
|
|
62
|
+
? getReviewScopeKey(ctx.cwd, source.args)
|
|
63
|
+
: undefined;
|
|
64
|
+
if (turnBasedScopeKey) {
|
|
65
|
+
const previousLines = getLastReviewSnapshot(ctx, turnBasedScopeKey);
|
|
66
|
+
if (previousLines) {
|
|
67
|
+
const marked = applyReviewedOverlay(reviewLines, previousLines);
|
|
68
|
+
ctx.ui.notify(
|
|
69
|
+
`Marked ${marked} reviewed line${marked === 1 ? "" : "s"}.`,
|
|
70
|
+
"info",
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
ctx.ui.notify(
|
|
74
|
+
"No reviewed baseline found. Press M to toggle the current hunk as reviewed.",
|
|
75
|
+
"info",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
54
79
|
await openReview(pi, ctx, {
|
|
55
80
|
title: source.label,
|
|
56
81
|
promptLabel: source.promptLabel,
|
|
57
82
|
cacheKey: getReviewCacheKey(ctx.cwd, source.label, diffText),
|
|
58
83
|
reviewLines,
|
|
84
|
+
markReviewed:
|
|
85
|
+
turnBasedScopeKey == null
|
|
86
|
+
? undefined
|
|
87
|
+
: (reviewedLines) =>
|
|
88
|
+
persistLastReviewSnapshot(pi, turnBasedScopeKey, reviewedLines),
|
|
59
89
|
buildPrompt: (comments) =>
|
|
60
90
|
buildReviewPrompt(comments, source.promptLabel),
|
|
61
91
|
});
|
|
@@ -113,6 +143,7 @@ async function openReview(
|
|
|
113
143
|
cacheKey: string;
|
|
114
144
|
reviewLines: ReviewLine[];
|
|
115
145
|
buildPrompt: (comments: ReviewComment[]) => string;
|
|
146
|
+
markReviewed?: (reviewedLines: ReviewLine[]) => void;
|
|
116
147
|
workspaceStore?: WorkspaceCommentStore;
|
|
117
148
|
},
|
|
118
149
|
): Promise<void> {
|
|
@@ -202,6 +233,9 @@ async function openReview(
|
|
|
202
233
|
persistCachedAsk(pi, options.cacheKey, updatedAsk);
|
|
203
234
|
},
|
|
204
235
|
() => summary(),
|
|
236
|
+
options.markReviewed
|
|
237
|
+
? (reviewedLines) => options.markReviewed?.(reviewedLines)
|
|
238
|
+
: undefined,
|
|
205
239
|
);
|
|
206
240
|
},
|
|
207
241
|
);
|
package/src/review/cache.ts
CHANGED
|
@@ -2,11 +2,17 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionCommandContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
PersistedAsk,
|
|
7
|
+
ReviewComment,
|
|
8
|
+
ReviewLine,
|
|
9
|
+
ReviewSnapshotLine,
|
|
10
|
+
} from "./types.ts";
|
|
6
11
|
|
|
7
12
|
const REVIEW_COMMENT_CACHE_ENTRY = "pi-diff-review-cache";
|
|
8
13
|
const REVIEW_EXPLANATION_CACHE_ENTRY = "pi-diff-review-explanation-cache";
|
|
9
14
|
const REVIEW_ASK_CACHE_ENTRY = "pi-diff-review-ask-cache";
|
|
15
|
+
const REVIEW_SNAPSHOT_CACHE_ENTRY = "pi-diff-review-snapshot-cache";
|
|
10
16
|
|
|
11
17
|
type ReviewCommentCacheEntry = {
|
|
12
18
|
cacheKey: string;
|
|
@@ -26,6 +32,12 @@ type ReviewAskCacheEntry = {
|
|
|
26
32
|
updatedAt: number;
|
|
27
33
|
};
|
|
28
34
|
|
|
35
|
+
type ReviewSnapshotCacheEntry = {
|
|
36
|
+
scopeKey: string;
|
|
37
|
+
lines: ReviewSnapshotLine[];
|
|
38
|
+
updatedAt: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
29
41
|
export function getCachedComments(
|
|
30
42
|
ctx: ExtensionCommandContext,
|
|
31
43
|
cacheKey: string,
|
|
@@ -169,3 +181,62 @@ export function persistCachedAsk(
|
|
|
169
181
|
updatedAt: Date.now(),
|
|
170
182
|
} satisfies ReviewAskCacheEntry);
|
|
171
183
|
}
|
|
184
|
+
|
|
185
|
+
export function getLastReviewSnapshot(
|
|
186
|
+
ctx: ExtensionCommandContext,
|
|
187
|
+
scopeKey: string,
|
|
188
|
+
): ReviewSnapshotLine[] | undefined {
|
|
189
|
+
let latest: ReviewSnapshotCacheEntry | undefined;
|
|
190
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
191
|
+
if (
|
|
192
|
+
entry.type !== "custom" ||
|
|
193
|
+
entry.customType !== REVIEW_SNAPSHOT_CACHE_ENTRY
|
|
194
|
+
) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const data = entry.data as Partial<ReviewSnapshotCacheEntry> | undefined;
|
|
199
|
+
if (data?.scopeKey !== scopeKey || !Array.isArray(data.lines)) continue;
|
|
200
|
+
|
|
201
|
+
const lines = data.lines.filter(isReviewSnapshotLine);
|
|
202
|
+
if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
|
|
203
|
+
latest = {
|
|
204
|
+
scopeKey: data.scopeKey,
|
|
205
|
+
lines,
|
|
206
|
+
updatedAt: data.updatedAt ?? 0,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return latest?.lines;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function persistLastReviewSnapshot(
|
|
215
|
+
pi: ExtensionAPI,
|
|
216
|
+
scopeKey: string,
|
|
217
|
+
lines: ReviewLine[] | ReviewSnapshotLine[],
|
|
218
|
+
): void {
|
|
219
|
+
pi.appendEntry(REVIEW_SNAPSHOT_CACHE_ENTRY, {
|
|
220
|
+
scopeKey,
|
|
221
|
+
lines: lines.map((line) => ({
|
|
222
|
+
kind: line.kind,
|
|
223
|
+
text: line.text,
|
|
224
|
+
filePath: line.filePath,
|
|
225
|
+
oldLineNumber: line.oldLineNumber,
|
|
226
|
+
newLineNumber: line.newLineNumber,
|
|
227
|
+
})),
|
|
228
|
+
updatedAt: Date.now(),
|
|
229
|
+
} satisfies ReviewSnapshotCacheEntry);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isReviewSnapshotLine(value: unknown): value is ReviewSnapshotLine {
|
|
233
|
+
if (!value || typeof value !== "object") return false;
|
|
234
|
+
const line = value as Partial<ReviewSnapshotLine>;
|
|
235
|
+
return (
|
|
236
|
+
typeof line.kind === "string" &&
|
|
237
|
+
typeof line.text === "string" &&
|
|
238
|
+
(line.filePath == null || typeof line.filePath === "string") &&
|
|
239
|
+
(line.oldLineNumber == null || typeof line.oldLineNumber === "number") &&
|
|
240
|
+
(line.newLineNumber == null || typeof line.newLineNumber === "number")
|
|
241
|
+
);
|
|
242
|
+
}
|
package/src/review/component.ts
CHANGED
|
@@ -31,6 +31,12 @@ import { ReviewSearchState } from "./search.ts";
|
|
|
31
31
|
import { buildReviewFileIndex, type ReviewFileSection } from "./files.ts";
|
|
32
32
|
import { padToWidth, lineNumberCell } from "../render/utils.ts";
|
|
33
33
|
import { buildSplitDiffRows } from "../diff/split.ts";
|
|
34
|
+
import {
|
|
35
|
+
areAllChangedLinesReviewed,
|
|
36
|
+
clearReviewedOverlay,
|
|
37
|
+
getReviewedLines,
|
|
38
|
+
markChangedLinesReviewed,
|
|
39
|
+
} from "../diff/turn-based-overlay.ts";
|
|
34
40
|
import type {
|
|
35
41
|
DiffRenderMode,
|
|
36
42
|
PersistedAsk,
|
|
@@ -55,8 +61,8 @@ type InlineBoxRow = {
|
|
|
55
61
|
|
|
56
62
|
type AnnotatedDiffRow =
|
|
57
63
|
| { kind: "file-header"; file: ReviewFileSection }
|
|
58
|
-
| { kind: "diff"; lineIndex: number }
|
|
59
|
-
| { kind: "split"; splitRowIndex: number }
|
|
64
|
+
| { kind: "diff"; lineIndex: number; segmentIndex: number }
|
|
65
|
+
| { kind: "split"; splitRowIndex: number; segmentIndex: number }
|
|
60
66
|
| InlineBoxRow;
|
|
61
67
|
|
|
62
68
|
const HELP_COMMANDS = [
|
|
@@ -71,12 +77,14 @@ const HELP_COMMANDS = [
|
|
|
71
77
|
["/", "search diff lines"],
|
|
72
78
|
["n / N", "jump between search matches"],
|
|
73
79
|
["J / K", "extend highlighted selection"],
|
|
80
|
+
["M", "toggle current hunk reviewed"],
|
|
74
81
|
["c", "add or edit a line or range comment"],
|
|
75
82
|
["C", "add or edit an overall diff comment"],
|
|
76
83
|
["x", "delete the current line or range comment"],
|
|
77
84
|
["t", "toggle the file sidebar"],
|
|
78
85
|
["s", "toggle inline comments and explanations"],
|
|
79
86
|
["v", "toggle unified or split rendering"],
|
|
87
|
+
["w", "toggle line wrap for long diff lines"],
|
|
80
88
|
["?", "toggle AI explanation for current hunk"],
|
|
81
89
|
["a", "ask a question about the current hunk"],
|
|
82
90
|
["Enter", "submit comments, save edits, or jump to search result"],
|
|
@@ -114,6 +122,7 @@ export class ReviewComponent {
|
|
|
114
122
|
private annotatedRowsEditMode = false;
|
|
115
123
|
private annotatedRowsEditingCommentKey?: string;
|
|
116
124
|
private annotatedRowsMode?: DiffRenderMode;
|
|
125
|
+
private annotatedRowsLineWrapEnabled = false;
|
|
117
126
|
private annotatedRowsInlineAnnotationsVisible = true;
|
|
118
127
|
private annotatedRowsVisibleExplanationCount = 0;
|
|
119
128
|
private annotatedRowByLineIndex?: number[];
|
|
@@ -134,6 +143,7 @@ export class ReviewComponent {
|
|
|
134
143
|
private getWorkspaceCommentSummary?: (
|
|
135
144
|
comments: Map<string, ReviewComment>,
|
|
136
145
|
) => WorkspaceCommentSummary | undefined,
|
|
146
|
+
private onMarkReviewed?: (reviewedLines: ReviewLine[]) => void,
|
|
137
147
|
) {
|
|
138
148
|
const firstCommentable = this.lines.findIndex((line) => line.commentable);
|
|
139
149
|
this.navigation = new ReviewNavigationState(
|
|
@@ -258,6 +268,10 @@ export class ReviewComponent {
|
|
|
258
268
|
return this.navigation.diffRenderMode;
|
|
259
269
|
}
|
|
260
270
|
|
|
271
|
+
private get lineWrapEnabled(): boolean {
|
|
272
|
+
return this.navigation.lineWrapEnabled;
|
|
273
|
+
}
|
|
274
|
+
|
|
261
275
|
handleInput(data: string): void {
|
|
262
276
|
if (this.helpVisible) {
|
|
263
277
|
if (data === "h" || matchesKey(data, "escape")) {
|
|
@@ -330,6 +344,10 @@ export class ReviewComponent {
|
|
|
330
344
|
this.toggleDiffRenderMode();
|
|
331
345
|
return;
|
|
332
346
|
}
|
|
347
|
+
if (data === "w") {
|
|
348
|
+
this.toggleLineWrap();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
333
351
|
if (data === "?") {
|
|
334
352
|
this.toggleExplanationPane();
|
|
335
353
|
return;
|
|
@@ -390,6 +408,10 @@ export class ReviewComponent {
|
|
|
390
408
|
this.extendSelection(-1);
|
|
391
409
|
return;
|
|
392
410
|
}
|
|
411
|
+
if (data === "M") {
|
|
412
|
+
this.markCurrentDiffReviewed();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
393
415
|
if (data === "n") {
|
|
394
416
|
if (this.search.query) {
|
|
395
417
|
this.jumpSearch(1);
|
|
@@ -478,7 +500,12 @@ export class ReviewComponent {
|
|
|
478
500
|
): string {
|
|
479
501
|
const visibleLineCount = this.getVisibleLineIndexes().length;
|
|
480
502
|
const summaryCount = this.explanationController.explanations.size;
|
|
481
|
-
const
|
|
503
|
+
const reviewedOverlayCount = this.lines.filter(
|
|
504
|
+
(line) => line.reviewedOverlay,
|
|
505
|
+
).length;
|
|
506
|
+
const reviewedOverlayText =
|
|
507
|
+
reviewedOverlayCount > 0 ? ` • ${reviewedOverlayCount} reviewed` : "";
|
|
508
|
+
const base = `${visibleLineCount}/${this.lines.length} lines • ${this.fileIndex.sections.length} files • ${this.comments.size} comments • ${summaryCount} summaries${reviewedOverlayText}${this.formatWorkspaceSummary(workspaceSummary)}`;
|
|
482
509
|
|
|
483
510
|
if (this.editMode) {
|
|
484
511
|
return `${base} • editing ${this.editingCommentKey === GLOBAL_COMMENT_KEY ? "overall comment" : "inline comment"}`;
|
|
@@ -626,6 +653,7 @@ export class ReviewComponent {
|
|
|
626
653
|
this.renderDiffLine(
|
|
627
654
|
line,
|
|
628
655
|
index,
|
|
656
|
+
annotated.segmentIndex,
|
|
629
657
|
width,
|
|
630
658
|
index === this.selected,
|
|
631
659
|
this.getSelectionBounds(),
|
|
@@ -635,7 +663,13 @@ export class ReviewComponent {
|
|
|
635
663
|
}
|
|
636
664
|
|
|
637
665
|
if (annotated.kind === "split") {
|
|
638
|
-
output.push(
|
|
666
|
+
output.push(
|
|
667
|
+
this.renderSplitDiffRowAt(
|
|
668
|
+
annotated.splitRowIndex,
|
|
669
|
+
annotated.segmentIndex,
|
|
670
|
+
width,
|
|
671
|
+
),
|
|
672
|
+
);
|
|
639
673
|
continue;
|
|
640
674
|
}
|
|
641
675
|
|
|
@@ -653,6 +687,7 @@ export class ReviewComponent {
|
|
|
653
687
|
this.annotatedRowsEditMode === this.editMode &&
|
|
654
688
|
this.annotatedRowsEditingCommentKey === this.editingCommentKey &&
|
|
655
689
|
this.annotatedRowsMode === this.diffRenderMode &&
|
|
690
|
+
this.annotatedRowsLineWrapEnabled === this.lineWrapEnabled &&
|
|
656
691
|
this.annotatedRowsInlineAnnotationsVisible ===
|
|
657
692
|
this.inlineAnnotationsVisible &&
|
|
658
693
|
this.annotatedRowsVisibleExplanationCount ===
|
|
@@ -682,6 +717,7 @@ export class ReviewComponent {
|
|
|
682
717
|
this.annotatedRowsEditMode = this.editMode;
|
|
683
718
|
this.annotatedRowsEditingCommentKey = this.editingCommentKey;
|
|
684
719
|
this.annotatedRowsMode = this.diffRenderMode;
|
|
720
|
+
this.annotatedRowsLineWrapEnabled = this.lineWrapEnabled;
|
|
685
721
|
this.annotatedRowsInlineAnnotationsVisible = this.inlineAnnotationsVisible;
|
|
686
722
|
this.annotatedRowsVisibleExplanationCount =
|
|
687
723
|
this.visibleExplanationKeys.size;
|
|
@@ -702,7 +738,18 @@ export class ReviewComponent {
|
|
|
702
738
|
index++
|
|
703
739
|
) {
|
|
704
740
|
rowByLineIndex[index] = rows.length;
|
|
705
|
-
|
|
741
|
+
const segmentCount = this.getDiffRowSegmentCount(
|
|
742
|
+
this.lines[index]!,
|
|
743
|
+
index,
|
|
744
|
+
width,
|
|
745
|
+
);
|
|
746
|
+
for (
|
|
747
|
+
let segmentIndex = 0;
|
|
748
|
+
segmentIndex < segmentCount;
|
|
749
|
+
segmentIndex++
|
|
750
|
+
) {
|
|
751
|
+
rows.push({ kind: "diff", lineIndex: index, segmentIndex });
|
|
752
|
+
}
|
|
706
753
|
|
|
707
754
|
if (!this.inlineAnnotationsVisible) continue;
|
|
708
755
|
|
|
@@ -736,7 +783,14 @@ export class ReviewComponent {
|
|
|
736
783
|
rowByLineIndex[lineIndex] = rows.length;
|
|
737
784
|
}
|
|
738
785
|
|
|
739
|
-
|
|
786
|
+
const segmentCount = this.getSplitRowSegmentCount(splitRow, width);
|
|
787
|
+
for (
|
|
788
|
+
let segmentIndex = 0;
|
|
789
|
+
segmentIndex < segmentCount;
|
|
790
|
+
segmentIndex++
|
|
791
|
+
) {
|
|
792
|
+
rows.push({ kind: "split", splitRowIndex, segmentIndex });
|
|
793
|
+
}
|
|
740
794
|
if (!this.inlineAnnotationsVisible) continue;
|
|
741
795
|
|
|
742
796
|
for (const lineIndex of lineIndexes) {
|
|
@@ -1183,7 +1237,11 @@ export class ReviewComponent {
|
|
|
1183
1237
|
);
|
|
1184
1238
|
}
|
|
1185
1239
|
|
|
1186
|
-
private renderSplitDiffRowAt(
|
|
1240
|
+
private renderSplitDiffRowAt(
|
|
1241
|
+
splitRowIndex: number,
|
|
1242
|
+
segmentIndex: number,
|
|
1243
|
+
width: number,
|
|
1244
|
+
): string {
|
|
1187
1245
|
const splitRow = this.getSplitDiffRows()[splitRowIndex];
|
|
1188
1246
|
if (!splitRow) return " ".repeat(width);
|
|
1189
1247
|
|
|
@@ -1191,6 +1249,7 @@ export class ReviewComponent {
|
|
|
1191
1249
|
return this.renderDiffLine(
|
|
1192
1250
|
splitRow.cell.line,
|
|
1193
1251
|
splitRow.cell.index,
|
|
1252
|
+
segmentIndex,
|
|
1194
1253
|
width,
|
|
1195
1254
|
splitRow.cell.index === this.selected,
|
|
1196
1255
|
this.getSelectionBounds(),
|
|
@@ -1201,10 +1260,15 @@ export class ReviewComponent {
|
|
|
1201
1260
|
const leftWidth = Math.max(10, Math.floor((width - separatorWidth) / 2));
|
|
1202
1261
|
const rightWidth = Math.max(10, width - leftWidth - separatorWidth);
|
|
1203
1262
|
const left = splitRow.left
|
|
1204
|
-
? this.renderSplitDiffCell(splitRow.left, leftWidth, "left")
|
|
1263
|
+
? this.renderSplitDiffCell(splitRow.left, leftWidth, "left", segmentIndex)
|
|
1205
1264
|
: " ".repeat(leftWidth);
|
|
1206
1265
|
const right = splitRow.right
|
|
1207
|
-
? this.renderSplitDiffCell(
|
|
1266
|
+
? this.renderSplitDiffCell(
|
|
1267
|
+
splitRow.right,
|
|
1268
|
+
rightWidth,
|
|
1269
|
+
"right",
|
|
1270
|
+
segmentIndex,
|
|
1271
|
+
)
|
|
1208
1272
|
: " ".repeat(rightWidth);
|
|
1209
1273
|
return truncateToWidth(
|
|
1210
1274
|
`${padToWidth(left, leftWidth)}${this.theme.fg("borderMuted", " │ ")}${padToWidth(right, rightWidth)}`,
|
|
@@ -1212,6 +1276,175 @@ export class ReviewComponent {
|
|
|
1212
1276
|
);
|
|
1213
1277
|
}
|
|
1214
1278
|
|
|
1279
|
+
private getDiffRowSegmentCount(
|
|
1280
|
+
line: ReviewLine,
|
|
1281
|
+
index: number,
|
|
1282
|
+
width: number,
|
|
1283
|
+
): number {
|
|
1284
|
+
return this.getDiffRowSegments(line, index, width).length;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
private getSplitRowSegmentCount(row: SplitDiffRow, width: number): number {
|
|
1288
|
+
if (row.kind === "full") {
|
|
1289
|
+
return this.getDiffRowSegmentCount(row.cell.line, row.cell.index, width);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const separatorWidth = 3;
|
|
1293
|
+
const leftWidth = Math.max(10, Math.floor((width - separatorWidth) / 2));
|
|
1294
|
+
const rightWidth = Math.max(10, width - leftWidth - separatorWidth);
|
|
1295
|
+
return Math.max(
|
|
1296
|
+
row.left
|
|
1297
|
+
? this.getSplitDiffCellSegments(row.left, leftWidth, "left").length
|
|
1298
|
+
: 1,
|
|
1299
|
+
row.right
|
|
1300
|
+
? this.getSplitDiffCellSegments(row.right, rightWidth, "right").length
|
|
1301
|
+
: 1,
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
private getDiffRowSegments(
|
|
1306
|
+
line: ReviewLine,
|
|
1307
|
+
index: number,
|
|
1308
|
+
width: number,
|
|
1309
|
+
): string[] {
|
|
1310
|
+
const lineMark = this.getLineMark(line, index);
|
|
1311
|
+
const numbers = `${lineNumberCell(line.oldLineNumber)} ${lineNumberCell(line.newLineNumber)}`;
|
|
1312
|
+
const prefix = this.styleDiffPrefix(line, `${lineMark} ${numbers} `);
|
|
1313
|
+
const continuationPrefix = this.styleDiffPrefix(
|
|
1314
|
+
line,
|
|
1315
|
+
`${lineMark} ${lineNumberCell()} ${lineNumberCell()} `,
|
|
1316
|
+
);
|
|
1317
|
+
return this.wrapDiffContentSegments(
|
|
1318
|
+
this.getRenderedDiffContent(line, index),
|
|
1319
|
+
prefix,
|
|
1320
|
+
continuationPrefix,
|
|
1321
|
+
width,
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
private getSplitDiffCellSegments(
|
|
1326
|
+
cell: SplitDiffCell,
|
|
1327
|
+
width: number,
|
|
1328
|
+
side: "left" | "right",
|
|
1329
|
+
): string[] {
|
|
1330
|
+
const { line, index } = cell;
|
|
1331
|
+
const lineMark = this.getLineMark(line, index);
|
|
1332
|
+
const lineNumber =
|
|
1333
|
+
side === "left" ? line.oldLineNumber : line.newLineNumber;
|
|
1334
|
+
const prefix = this.styleDiffPrefix(
|
|
1335
|
+
line,
|
|
1336
|
+
`${lineMark} ${lineNumberCell(lineNumber)} `,
|
|
1337
|
+
);
|
|
1338
|
+
const continuationPrefix = this.styleDiffPrefix(
|
|
1339
|
+
line,
|
|
1340
|
+
`${lineMark} ${lineNumberCell()} `,
|
|
1341
|
+
);
|
|
1342
|
+
return this.wrapDiffContentSegments(
|
|
1343
|
+
this.getRenderedDiffContent(line, index),
|
|
1344
|
+
prefix,
|
|
1345
|
+
continuationPrefix,
|
|
1346
|
+
width,
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
private getRenderedDiffContent(line: ReviewLine, index: number): string {
|
|
1351
|
+
switch (line.kind) {
|
|
1352
|
+
case "add":
|
|
1353
|
+
case "remove":
|
|
1354
|
+
case "context":
|
|
1355
|
+
return this.getHighlightedDisplayText(line, index);
|
|
1356
|
+
case "hunk":
|
|
1357
|
+
return this.theme.fg("accent", this.getDisplayText(line));
|
|
1358
|
+
default:
|
|
1359
|
+
return this.theme.fg("muted", this.getDisplayText(line));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
private styleDiffPrefix(line: ReviewLine, text: string): string {
|
|
1364
|
+
switch (line.kind) {
|
|
1365
|
+
case "add":
|
|
1366
|
+
return this.theme.fg("toolDiffAdded", text);
|
|
1367
|
+
case "remove":
|
|
1368
|
+
return this.theme.fg("toolDiffRemoved", text);
|
|
1369
|
+
case "context":
|
|
1370
|
+
return this.theme.fg("toolDiffContext", text);
|
|
1371
|
+
case "hunk":
|
|
1372
|
+
return this.theme.fg("accent", text);
|
|
1373
|
+
default:
|
|
1374
|
+
return this.theme.fg("muted", text);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
private getLineMark(line: ReviewLine, index: number): string {
|
|
1379
|
+
const hasComment = this.getCommentKeysForLine(index).length > 0;
|
|
1380
|
+
if (hasComment) return this.theme.fg("borderAccent", "│");
|
|
1381
|
+
return " ";
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
private markCurrentDiffReviewed(): void {
|
|
1385
|
+
if (!this.onMarkReviewed) return;
|
|
1386
|
+
const range = this.getCurrentHunkRange();
|
|
1387
|
+
if (!range) return;
|
|
1388
|
+
|
|
1389
|
+
const hunkLines = this.lines.slice(range.start, range.end + 1);
|
|
1390
|
+
if (areAllChangedLinesReviewed(hunkLines)) {
|
|
1391
|
+
clearReviewedOverlay(hunkLines);
|
|
1392
|
+
} else {
|
|
1393
|
+
markChangedLinesReviewed(hunkLines);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
this.onMarkReviewed(getReviewedLines(this.lines));
|
|
1397
|
+
this.invalidateAnnotatedRows();
|
|
1398
|
+
this.tui.requestRender(true);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
private getCurrentHunkRange(): SelectionBounds | undefined {
|
|
1402
|
+
const selectedLine = this.lines[this.selected];
|
|
1403
|
+
if (!selectedLine?.filePath || !selectedLine.hunkLabel) return undefined;
|
|
1404
|
+
|
|
1405
|
+
let start = this.selected;
|
|
1406
|
+
while (
|
|
1407
|
+
start > 0 &&
|
|
1408
|
+
this.lines[start - 1]?.filePath === selectedLine.filePath &&
|
|
1409
|
+
this.lines[start - 1]?.hunkLabel === selectedLine.hunkLabel
|
|
1410
|
+
) {
|
|
1411
|
+
start--;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const end = this.getHunkEndIndex(this.selected);
|
|
1415
|
+
return end == null ? undefined : { start, end };
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
private wrapDiffContentSegments(
|
|
1419
|
+
content: string,
|
|
1420
|
+
prefix: string,
|
|
1421
|
+
continuationPrefix: string,
|
|
1422
|
+
width: number,
|
|
1423
|
+
): string[] {
|
|
1424
|
+
const firstLineWidth = Math.max(1, width - visibleWidth(prefix));
|
|
1425
|
+
const continuationWidth = Math.max(
|
|
1426
|
+
1,
|
|
1427
|
+
width - visibleWidth(continuationPrefix),
|
|
1428
|
+
);
|
|
1429
|
+
|
|
1430
|
+
if (!this.lineWrapEnabled) {
|
|
1431
|
+
return [`${prefix}${truncateToWidth(content, firstLineWidth)}`];
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const firstSegments = wrapTextWithAnsi(content, firstLineWidth);
|
|
1435
|
+
if (firstSegments.length === 0) return [prefix];
|
|
1436
|
+
if (firstSegments.length === 1) return [`${prefix}${firstSegments[0]}`];
|
|
1437
|
+
|
|
1438
|
+
const [firstSegment, ...remaining] = firstSegments;
|
|
1439
|
+
const wrapped = [`${prefix}${firstSegment}`];
|
|
1440
|
+
for (const segment of remaining) {
|
|
1441
|
+
for (const continuation of wrapTextWithAnsi(segment, continuationWidth)) {
|
|
1442
|
+
wrapped.push(`${continuationPrefix}${continuation}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return wrapped;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1215
1448
|
private getContentHeight(): number {
|
|
1216
1449
|
const terminalRows = this.tui.terminal?.rows ?? 24;
|
|
1217
1450
|
const headerHeight = 3;
|
|
@@ -1326,6 +1559,12 @@ export class ReviewComponent {
|
|
|
1326
1559
|
this.tui.requestRender(true);
|
|
1327
1560
|
}
|
|
1328
1561
|
|
|
1562
|
+
private toggleLineWrap(): void {
|
|
1563
|
+
this.navigation.toggleLineWrap();
|
|
1564
|
+
this.invalidateAnnotatedRows();
|
|
1565
|
+
this.tui.requestRender(true);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1329
1568
|
private toggleInlineAnnotations(): void {
|
|
1330
1569
|
this.inlineAnnotationsVisible = !this.inlineAnnotationsVisible;
|
|
1331
1570
|
this.invalidateAnnotatedRows();
|
|
@@ -1600,19 +1839,18 @@ export class ReviewComponent {
|
|
|
1600
1839
|
cell: SplitDiffCell,
|
|
1601
1840
|
width: number,
|
|
1602
1841
|
side: "left" | "right",
|
|
1842
|
+
segmentIndex: number,
|
|
1603
1843
|
): string {
|
|
1604
1844
|
const { line, index } = cell;
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
side === "left" ? line.oldLineNumber : line.newLineNumber;
|
|
1609
|
-
const prefix = `${commentMark} ${lineNumberCell(lineNumber)} `;
|
|
1610
|
-
let styled = this.renderDiffRowContent(line, prefix, index);
|
|
1611
|
-
|
|
1612
|
-
styled = truncateToWidth(styled, width);
|
|
1845
|
+
const styled =
|
|
1846
|
+
this.getSplitDiffCellSegments(cell, width, side)[segmentIndex] ??
|
|
1847
|
+
" ".repeat(width);
|
|
1613
1848
|
const selection = this.getSelectionBounds();
|
|
1614
1849
|
const inSelection =
|
|
1615
1850
|
selection != null && index >= selection.start && index <= selection.end;
|
|
1851
|
+
if (line.reviewedOverlay) {
|
|
1852
|
+
return this.applyReviewedBackground(styled, width);
|
|
1853
|
+
}
|
|
1616
1854
|
if (index === this.selected || inSelection) {
|
|
1617
1855
|
return this.theme.bg("selectedBg", padToWidth(styled, width));
|
|
1618
1856
|
}
|
|
@@ -1651,15 +1889,24 @@ export class ReviewComponent {
|
|
|
1651
1889
|
styled: string,
|
|
1652
1890
|
width: number,
|
|
1653
1891
|
): string {
|
|
1892
|
+
if (line.reviewedOverlay) {
|
|
1893
|
+
return this.applyReviewedBackground(styled, width);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
const padded = padToWidth(styled, width);
|
|
1654
1897
|
if (line.kind === "add") {
|
|
1655
|
-
return this.theme.bg("toolSuccessBg",
|
|
1898
|
+
return this.theme.bg("toolSuccessBg", padded);
|
|
1656
1899
|
}
|
|
1657
1900
|
if (line.kind === "remove") {
|
|
1658
|
-
return this.theme.bg("toolErrorBg",
|
|
1901
|
+
return this.theme.bg("toolErrorBg", padded);
|
|
1659
1902
|
}
|
|
1660
1903
|
return styled;
|
|
1661
1904
|
}
|
|
1662
1905
|
|
|
1906
|
+
private applyReviewedBackground(styled: string, width: number): string {
|
|
1907
|
+
return `\x1b[48;2;38;68;92m${padToWidth(styled, width)}\x1b[49m`;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1663
1910
|
private renderDiffRowContent(
|
|
1664
1911
|
line: ReviewLine,
|
|
1665
1912
|
prefix: string,
|
|
@@ -1734,19 +1981,19 @@ export class ReviewComponent {
|
|
|
1734
1981
|
private renderDiffLine(
|
|
1735
1982
|
line: ReviewLine,
|
|
1736
1983
|
index: number,
|
|
1984
|
+
segmentIndex: number,
|
|
1737
1985
|
width: number,
|
|
1738
1986
|
selected: boolean,
|
|
1739
1987
|
selection?: SelectionBounds,
|
|
1740
1988
|
): string {
|
|
1741
|
-
const
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
const prefix = `${commentMark} ${numbers} `;
|
|
1745
|
-
let styled = this.renderDiffRowContent(line, prefix, index);
|
|
1746
|
-
|
|
1747
|
-
styled = truncateToWidth(styled, width);
|
|
1989
|
+
const styled =
|
|
1990
|
+
this.getDiffRowSegments(line, index, width)[segmentIndex] ??
|
|
1991
|
+
" ".repeat(width);
|
|
1748
1992
|
const inSelection =
|
|
1749
1993
|
selection != null && index >= selection.start && index <= selection.end;
|
|
1994
|
+
if (line.reviewedOverlay) {
|
|
1995
|
+
return this.applyReviewedBackground(styled, width);
|
|
1996
|
+
}
|
|
1750
1997
|
if (selected || inSelection) {
|
|
1751
1998
|
return this.theme.bg("selectedBg", padToWidth(styled, width));
|
|
1752
1999
|
}
|
package/src/review/navigation.ts
CHANGED
|
@@ -10,6 +10,7 @@ export class ReviewNavigationState {
|
|
|
10
10
|
scrollTop = 0;
|
|
11
11
|
selectionAnchor?: number;
|
|
12
12
|
diffRenderMode: DiffRenderMode = "unified";
|
|
13
|
+
lineWrapEnabled = false;
|
|
13
14
|
|
|
14
15
|
constructor(
|
|
15
16
|
private readonly lineCount: number,
|
|
@@ -84,6 +85,12 @@ export class ReviewNavigationState {
|
|
|
84
85
|
return this.diffRenderMode;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
toggleLineWrap(): boolean {
|
|
89
|
+
this.lineWrapEnabled = !this.lineWrapEnabled;
|
|
90
|
+
this.scrollTop = 0;
|
|
91
|
+
return this.lineWrapEnabled;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
ensureScroll(
|
|
88
95
|
viewportHeight: number,
|
|
89
96
|
selectedDisplayRow: number,
|
package/src/review/types.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type ReviewLine = {
|
|
|
25
25
|
newLineNumber?: number;
|
|
26
26
|
commentable: boolean;
|
|
27
27
|
hunkLabel?: string;
|
|
28
|
+
reviewedOverlay?: boolean;
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export type ReviewResult =
|
|
@@ -40,8 +41,14 @@ export type DiffSource = {
|
|
|
40
41
|
label: string;
|
|
41
42
|
promptLabel: string;
|
|
42
43
|
args: string[];
|
|
44
|
+
turnBased?: boolean;
|
|
43
45
|
};
|
|
44
46
|
|
|
47
|
+
export type ReviewSnapshotLine = Pick<
|
|
48
|
+
ReviewLine,
|
|
49
|
+
"kind" | "text" | "filePath" | "oldLineNumber" | "newLineNumber"
|
|
50
|
+
>;
|
|
51
|
+
|
|
45
52
|
export type PersistedAsk = {
|
|
46
53
|
scopeKey: string;
|
|
47
54
|
anchorLineId: string;
|