pi-diff-review 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -17
- package/package.json +1 -1
- package/src/{diff-parser.ts → diff/parser.ts} +1 -1
- package/src/{diff-source.ts → diff/source.ts} +1 -1
- package/src/{split-diff.ts → diff/split.ts} +5 -1
- package/src/{explanation-controller.ts → explanation/controller.ts} +2 -2
- package/src/index.ts +10 -6
- package/src/{review-component.ts → review/component.ts} +81 -14
- package/src/review/search.ts +109 -0
- /package/src/{explain.ts → explanation/explainer.ts} +0 -0
- /package/src/{render-utils.ts → render/utils.ts} +0 -0
- /package/src/{comment-manager.ts → review/comments.ts} +0 -0
- /package/src/{review-navigation.ts → review/navigation.ts} +0 -0
- /package/src/{prompt.ts → review/prompt.ts} +0 -0
- /package/src/{types.ts → review/types.ts} +0 -0
package/README.md
CHANGED
|
@@ -68,6 +68,7 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
|
|
|
68
68
|
- `t` toggles inline comments/explanations
|
|
69
69
|
- `v` toggles the diff between unified and side-by-side split rendering
|
|
70
70
|
- `?` toggles an AI-generated explanation for the current hunk
|
|
71
|
+
- `/` searches diff lines; `n/N` moves between matches while a search is active
|
|
71
72
|
- `J/K` to extend a highlighted selection into a comment range
|
|
72
73
|
- `esc` clears the active selection, or exits review when no selection is active
|
|
73
74
|
- `n/p` to jump hunks
|
|
@@ -78,21 +79,6 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
|
|
|
78
79
|
- Comments are cached per session and restored when reopening the same diff
|
|
79
80
|
- `q` to exit
|
|
80
81
|
|
|
81
|
-
##
|
|
82
|
+
## Contributing
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
pre-commit install
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Run the same checks manually with either:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
pre-commit run --all-files
|
|
93
|
-
npm run precommit
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Release
|
|
97
|
-
|
|
98
|
-
See [RELEASE.md](./RELEASE.md).
|
|
84
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development and release instructions.
|
package/package.json
CHANGED
|
@@ -2,8 +2,8 @@ import type {
|
|
|
2
2
|
DiffExplainer,
|
|
3
3
|
ExplanationScope,
|
|
4
4
|
ExplanationState,
|
|
5
|
-
} from "./
|
|
6
|
-
import type { ReviewLine, ReviewTui } from "
|
|
5
|
+
} from "./explainer.ts";
|
|
6
|
+
import type { ReviewLine, ReviewTui } from "../review/types.ts";
|
|
7
7
|
|
|
8
8
|
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
9
|
|
package/src/index.ts
CHANGED
|
@@ -3,12 +3,16 @@ import type {
|
|
|
3
3
|
ExtensionAPI,
|
|
4
4
|
ExtensionCommandContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { getDiff, parseDiffSource } from "./diff
|
|
7
|
-
import { parseDiff } from "./diff
|
|
8
|
-
import { PiModelDiffExplainer } from "./
|
|
9
|
-
import { buildReviewPrompt } from "./prompt.ts";
|
|
10
|
-
import { ReviewComponent } from "./review
|
|
11
|
-
import type {
|
|
6
|
+
import { getDiff, parseDiffSource } from "./diff/source.ts";
|
|
7
|
+
import { parseDiff } from "./diff/parser.ts";
|
|
8
|
+
import { PiModelDiffExplainer } from "./explanation/explainer.ts";
|
|
9
|
+
import { buildReviewPrompt } from "./review/prompt.ts";
|
|
10
|
+
import { ReviewComponent } from "./review/component.ts";
|
|
11
|
+
import type {
|
|
12
|
+
DiffSource,
|
|
13
|
+
ReviewComment,
|
|
14
|
+
ReviewResult,
|
|
15
|
+
} from "./review/types.ts";
|
|
12
16
|
|
|
13
17
|
const DIFF_REVIEW_CACHE_ENTRY = "pi-diff-review-cache";
|
|
14
18
|
const DIFF_REVIEW_EXPLANATION_CACHE_ENTRY = "pi-diff-review-explanation-cache";
|
|
@@ -9,22 +9,23 @@ import {
|
|
|
9
9
|
visibleWidth,
|
|
10
10
|
wrapTextWithAnsi,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
|
-
import type { DiffExplainer } from "
|
|
12
|
+
import type { DiffExplainer } from "../explanation/explainer.ts";
|
|
13
13
|
import {
|
|
14
14
|
GLOBAL_COMMENT_KEY,
|
|
15
15
|
buildCommentFromSelection,
|
|
16
16
|
buildCommentLineKeys,
|
|
17
17
|
buildGlobalComment,
|
|
18
18
|
getSelectionKey,
|
|
19
|
-
} from "./
|
|
19
|
+
} from "./comments.ts";
|
|
20
20
|
import {
|
|
21
21
|
ExplanationController,
|
|
22
22
|
getCurrentHunkScope,
|
|
23
|
-
} from "
|
|
23
|
+
} from "../explanation/controller.ts";
|
|
24
24
|
import { formatCommentLocation, formatLocation } from "./prompt.ts";
|
|
25
|
-
import { ReviewNavigationState } from "./
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
25
|
+
import { ReviewNavigationState } from "./navigation.ts";
|
|
26
|
+
import { ReviewSearchState } from "./search.ts";
|
|
27
|
+
import { padToWidth, lineNumberCell } from "../render/utils.ts";
|
|
28
|
+
import { buildSplitDiffRows } from "../diff/split.ts";
|
|
28
29
|
import type {
|
|
29
30
|
DiffRenderMode,
|
|
30
31
|
ReviewComment,
|
|
@@ -54,6 +55,7 @@ export class ReviewComponent {
|
|
|
54
55
|
private navigation: ReviewNavigationState;
|
|
55
56
|
private editMode = false;
|
|
56
57
|
private editingCommentKey?: string;
|
|
58
|
+
private search: ReviewSearchState;
|
|
57
59
|
|
|
58
60
|
private inlineAnnotationsVisible = true;
|
|
59
61
|
private visibleExplanationKeys = new Set<string>();
|
|
@@ -93,6 +95,7 @@ export class ReviewComponent {
|
|
|
93
95
|
firstCommentable >= 0 ? firstCommentable : 0,
|
|
94
96
|
);
|
|
95
97
|
this.lines.forEach((line, index) => this.lineIndexById.set(line.id, index));
|
|
98
|
+
this.search = new ReviewSearchState(this.lines);
|
|
96
99
|
this.explanationController = new ExplanationController(
|
|
97
100
|
tui,
|
|
98
101
|
explainer,
|
|
@@ -179,8 +182,15 @@ export class ReviewComponent {
|
|
|
179
182
|
return;
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
if (this.search.mode) {
|
|
186
|
+
this.handleSearchInput(data);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
182
190
|
if (matchesKey(data, "escape")) {
|
|
183
|
-
if (this.
|
|
191
|
+
if (this.search.query) {
|
|
192
|
+
this.clearSearch();
|
|
193
|
+
} else if (this.hasSelection()) {
|
|
184
194
|
this.clearSelection();
|
|
185
195
|
} else {
|
|
186
196
|
this.done({ action: "cancel" });
|
|
@@ -203,6 +213,10 @@ export class ReviewComponent {
|
|
|
203
213
|
this.toggleExplanationPane();
|
|
204
214
|
return;
|
|
205
215
|
}
|
|
216
|
+
if (data === "/") {
|
|
217
|
+
this.startSearchMode();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
206
220
|
if (matchesKey(data, "ctrl+d")) {
|
|
207
221
|
this.move(this.getPageMoveAmount());
|
|
208
222
|
return;
|
|
@@ -236,7 +250,15 @@ export class ReviewComponent {
|
|
|
236
250
|
return;
|
|
237
251
|
}
|
|
238
252
|
if (data === "n") {
|
|
239
|
-
this.
|
|
253
|
+
if (this.search.query) {
|
|
254
|
+
this.jumpSearch(1);
|
|
255
|
+
} else {
|
|
256
|
+
this.jumpHunk(1);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (data === "N" && this.search.query) {
|
|
261
|
+
this.jumpSearch(-1);
|
|
240
262
|
return;
|
|
241
263
|
}
|
|
242
264
|
if (data === "p") {
|
|
@@ -277,7 +299,7 @@ export class ReviewComponent {
|
|
|
277
299
|
? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • editing inline comment • Enter save • Esc/Ctrl+C cancel`
|
|
278
300
|
: this.hasSelection()
|
|
279
301
|
? `${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`,
|
|
302
|
+
: `${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 • / search • t annotations • v unified/split • ? explain • J/K extend • c comment • C overall • x delete • ${this.search.query ? "n/N search" : "n/p hunk"} • Enter submit • q quit`,
|
|
281
303
|
),
|
|
282
304
|
width,
|
|
283
305
|
),
|
|
@@ -846,6 +868,13 @@ export class ReviewComponent {
|
|
|
846
868
|
}
|
|
847
869
|
|
|
848
870
|
private getFooterText(selectedLine?: ReviewLine): string {
|
|
871
|
+
if (this.search.mode) {
|
|
872
|
+
return `Search: /${this.search.draftQuery} • Enter jump • Esc cancel`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const searchStatus = this.search.getStatusText(this.selected);
|
|
876
|
+
if (searchStatus) return searchStatus;
|
|
877
|
+
|
|
849
878
|
const selection = this.getSelectionBounds();
|
|
850
879
|
if (selection) {
|
|
851
880
|
const count = selection.end - selection.start + 1;
|
|
@@ -919,6 +948,28 @@ export class ReviewComponent {
|
|
|
919
948
|
this.tui.requestRender(true);
|
|
920
949
|
}
|
|
921
950
|
|
|
951
|
+
private startSearchMode(): void {
|
|
952
|
+
this.search.start();
|
|
953
|
+
this.tui.requestRender();
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private handleSearchInput(data: string): void {
|
|
957
|
+
const result = this.search.handleInput(data, this.selected);
|
|
958
|
+
if (result.selected != null) this.selected = result.selected;
|
|
959
|
+
this.tui.requestRender();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
private clearSearch(): void {
|
|
963
|
+
this.search.clear();
|
|
964
|
+
this.tui.requestRender();
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private jumpSearch(direction: 1 | -1): void {
|
|
968
|
+
const result = this.search.jump(direction, this.selected);
|
|
969
|
+
if (result.selected != null) this.selected = result.selected;
|
|
970
|
+
this.tui.requestRender();
|
|
971
|
+
}
|
|
972
|
+
|
|
922
973
|
private getSplitDiffRows(): SplitDiffRow[] {
|
|
923
974
|
if (this.splitRows) return this.splitRows;
|
|
924
975
|
|
|
@@ -968,11 +1019,11 @@ export class ReviewComponent {
|
|
|
968
1019
|
}
|
|
969
1020
|
|
|
970
1021
|
private getDisplayText(line: ReviewLine): string {
|
|
971
|
-
|
|
972
|
-
line.kind === "remove" ||
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1022
|
+
const raw =
|
|
1023
|
+
line.kind === "add" || line.kind === "remove" || line.kind === "context"
|
|
1024
|
+
? line.text.slice(1)
|
|
1025
|
+
: line.text;
|
|
1026
|
+
return expandTabs(raw);
|
|
976
1027
|
}
|
|
977
1028
|
|
|
978
1029
|
private applyDiffBackground(
|
|
@@ -1054,3 +1105,19 @@ export class ReviewComponent {
|
|
|
1054
1105
|
return getCurrentHunkScope(this.lines, this.selected);
|
|
1055
1106
|
}
|
|
1056
1107
|
}
|
|
1108
|
+
|
|
1109
|
+
function expandTabs(text: string, tabSize = 4): string {
|
|
1110
|
+
let result = "";
|
|
1111
|
+
let col = 0;
|
|
1112
|
+
for (const char of text) {
|
|
1113
|
+
if (char === "\t") {
|
|
1114
|
+
const spaces = tabSize - (col % tabSize);
|
|
1115
|
+
result += " ".repeat(spaces);
|
|
1116
|
+
col += spaces;
|
|
1117
|
+
} else {
|
|
1118
|
+
result += char;
|
|
1119
|
+
col++;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return result;
|
|
1123
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { ReviewLine } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export type SearchInputResult = {
|
|
5
|
+
selected?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class ReviewSearchState {
|
|
9
|
+
mode = false;
|
|
10
|
+
query = "";
|
|
11
|
+
draftQuery = "";
|
|
12
|
+
message = "";
|
|
13
|
+
|
|
14
|
+
constructor(private readonly lines: ReviewLine[]) {}
|
|
15
|
+
|
|
16
|
+
start(): void {
|
|
17
|
+
this.mode = true;
|
|
18
|
+
this.draftQuery = this.query;
|
|
19
|
+
this.message = "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
handleInput(data: string, selected: number): SearchInputResult {
|
|
23
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
24
|
+
this.mode = false;
|
|
25
|
+
this.draftQuery = "";
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (matchesKey(data, "enter")) {
|
|
30
|
+
const query = this.draftQuery.trim();
|
|
31
|
+
this.mode = false;
|
|
32
|
+
this.draftQuery = "";
|
|
33
|
+
this.query = query;
|
|
34
|
+
this.message = "";
|
|
35
|
+
return query ? this.jump(1, selected) : {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (matchesKey(data, "ctrl+u")) {
|
|
39
|
+
this.draftQuery = "";
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (matchesKey(data, "backspace") || data === "\u007f" || data === "\b") {
|
|
44
|
+
this.draftQuery = this.draftQuery.slice(0, -1);
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (data.length === 1 && data >= " " && data !== "\u007f") {
|
|
49
|
+
this.draftQuery += data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
clear(): void {
|
|
56
|
+
this.mode = false;
|
|
57
|
+
this.query = "";
|
|
58
|
+
this.draftQuery = "";
|
|
59
|
+
this.message = "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
jump(direction: 1 | -1, selected: number): SearchInputResult {
|
|
63
|
+
const query = this.query.trim();
|
|
64
|
+
if (!query) return {};
|
|
65
|
+
|
|
66
|
+
const matches = this.getMatchIndexes(query);
|
|
67
|
+
if (matches.length === 0) {
|
|
68
|
+
this.message = `No matches for /${query}`;
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.message = "";
|
|
73
|
+
const current =
|
|
74
|
+
direction === 1
|
|
75
|
+
? matches.find((index) => index > selected)
|
|
76
|
+
: [...matches].reverse().find((index) => index < selected);
|
|
77
|
+
return {
|
|
78
|
+
selected:
|
|
79
|
+
current ??
|
|
80
|
+
(direction === 1 ? matches[0]! : matches[matches.length - 1]!),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getStatusText(selected: number): string {
|
|
85
|
+
if (this.message) return this.message;
|
|
86
|
+
if (!this.query) return "";
|
|
87
|
+
|
|
88
|
+
const matches = this.getMatchIndexes(this.query);
|
|
89
|
+
if (matches.length === 0) return `No matches for /${this.query}`;
|
|
90
|
+
|
|
91
|
+
const current = matches.findIndex((index) => index === selected);
|
|
92
|
+
const position =
|
|
93
|
+
current >= 0
|
|
94
|
+
? `${current + 1}/${matches.length}`
|
|
95
|
+
: `${matches.length} matches`;
|
|
96
|
+
return `Search /${this.query} • ${position} • n next • N previous • Esc clear search`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private getMatchIndexes(query: string): number[] {
|
|
100
|
+
const needle = query.toLocaleLowerCase();
|
|
101
|
+
if (!needle) return [];
|
|
102
|
+
|
|
103
|
+
const matches: number[] = [];
|
|
104
|
+
this.lines.forEach((line, index) => {
|
|
105
|
+
if (line.text.toLocaleLowerCase().includes(needle)) matches.push(index);
|
|
106
|
+
});
|
|
107
|
+
return matches;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|