pi-diff-review 0.1.15 → 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 +2 -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} +45 -110
- 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
|
@@ -79,21 +79,6 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
|
|
|
79
79
|
- Comments are cached per session and restored when reopening the same diff
|
|
80
80
|
- `q` to exit
|
|
81
81
|
|
|
82
|
-
##
|
|
82
|
+
## Contributing
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
pre-commit install
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Run the same checks manually with either:
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
pre-commit run --all-files
|
|
94
|
-
npm run precommit
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Release
|
|
98
|
-
|
|
99
|
-
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,10 +55,7 @@ export class ReviewComponent {
|
|
|
54
55
|
private navigation: ReviewNavigationState;
|
|
55
56
|
private editMode = false;
|
|
56
57
|
private editingCommentKey?: string;
|
|
57
|
-
private
|
|
58
|
-
private searchQuery = "";
|
|
59
|
-
private draftSearchQuery = "";
|
|
60
|
-
private searchMessage = "";
|
|
58
|
+
private search: ReviewSearchState;
|
|
61
59
|
|
|
62
60
|
private inlineAnnotationsVisible = true;
|
|
63
61
|
private visibleExplanationKeys = new Set<string>();
|
|
@@ -97,6 +95,7 @@ export class ReviewComponent {
|
|
|
97
95
|
firstCommentable >= 0 ? firstCommentable : 0,
|
|
98
96
|
);
|
|
99
97
|
this.lines.forEach((line, index) => this.lineIndexById.set(line.id, index));
|
|
98
|
+
this.search = new ReviewSearchState(this.lines);
|
|
100
99
|
this.explanationController = new ExplanationController(
|
|
101
100
|
tui,
|
|
102
101
|
explainer,
|
|
@@ -183,13 +182,13 @@ export class ReviewComponent {
|
|
|
183
182
|
return;
|
|
184
183
|
}
|
|
185
184
|
|
|
186
|
-
if (this.
|
|
185
|
+
if (this.search.mode) {
|
|
187
186
|
this.handleSearchInput(data);
|
|
188
187
|
return;
|
|
189
188
|
}
|
|
190
189
|
|
|
191
190
|
if (matchesKey(data, "escape")) {
|
|
192
|
-
if (this.
|
|
191
|
+
if (this.search.query) {
|
|
193
192
|
this.clearSearch();
|
|
194
193
|
} else if (this.hasSelection()) {
|
|
195
194
|
this.clearSelection();
|
|
@@ -251,14 +250,14 @@ export class ReviewComponent {
|
|
|
251
250
|
return;
|
|
252
251
|
}
|
|
253
252
|
if (data === "n") {
|
|
254
|
-
if (this.
|
|
253
|
+
if (this.search.query) {
|
|
255
254
|
this.jumpSearch(1);
|
|
256
255
|
} else {
|
|
257
256
|
this.jumpHunk(1);
|
|
258
257
|
}
|
|
259
258
|
return;
|
|
260
259
|
}
|
|
261
|
-
if (data === "N" && this.
|
|
260
|
+
if (data === "N" && this.search.query) {
|
|
262
261
|
this.jumpSearch(-1);
|
|
263
262
|
return;
|
|
264
263
|
}
|
|
@@ -300,7 +299,7 @@ export class ReviewComponent {
|
|
|
300
299
|
? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • editing inline comment • Enter save • Esc/Ctrl+C cancel`
|
|
301
300
|
: this.hasSelection()
|
|
302
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`
|
|
303
|
-
: `${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.
|
|
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`,
|
|
304
303
|
),
|
|
305
304
|
width,
|
|
306
305
|
),
|
|
@@ -869,11 +868,11 @@ export class ReviewComponent {
|
|
|
869
868
|
}
|
|
870
869
|
|
|
871
870
|
private getFooterText(selectedLine?: ReviewLine): string {
|
|
872
|
-
if (this.
|
|
873
|
-
return `Search: /${this.
|
|
871
|
+
if (this.search.mode) {
|
|
872
|
+
return `Search: /${this.search.draftQuery} • Enter jump • Esc cancel`;
|
|
874
873
|
}
|
|
875
874
|
|
|
876
|
-
const searchStatus = this.
|
|
875
|
+
const searchStatus = this.search.getStatusText(this.selected);
|
|
877
876
|
if (searchStatus) return searchStatus;
|
|
878
877
|
|
|
879
878
|
const selection = this.getSelectionBounds();
|
|
@@ -886,21 +885,6 @@ export class ReviewComponent {
|
|
|
886
885
|
return `Selected: ${selectedLine ? formatLocation(selectedLine) : "(no selection)"}`;
|
|
887
886
|
}
|
|
888
887
|
|
|
889
|
-
private getSearchStatusText(): string {
|
|
890
|
-
if (this.searchMessage) return this.searchMessage;
|
|
891
|
-
if (!this.searchQuery) return "";
|
|
892
|
-
|
|
893
|
-
const matches = this.getSearchMatchIndexes(this.searchQuery);
|
|
894
|
-
if (matches.length === 0) return `No matches for /${this.searchQuery}`;
|
|
895
|
-
|
|
896
|
-
const current = matches.findIndex((index) => index === this.selected);
|
|
897
|
-
const position =
|
|
898
|
-
current >= 0
|
|
899
|
-
? `${current + 1}/${matches.length}`
|
|
900
|
-
: `${matches.length} matches`;
|
|
901
|
-
return `Search /${this.searchQuery} • ${position} • n next • N previous • Esc clear search`;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
888
|
private jumpHunk(direction: 1 | -1): void {
|
|
905
889
|
let index = this.selected + direction;
|
|
906
890
|
while (index >= 0 && index < this.lines.length) {
|
|
@@ -965,92 +949,27 @@ export class ReviewComponent {
|
|
|
965
949
|
}
|
|
966
950
|
|
|
967
951
|
private startSearchMode(): void {
|
|
968
|
-
this.
|
|
969
|
-
this.draftSearchQuery = this.searchQuery;
|
|
970
|
-
this.searchMessage = "";
|
|
952
|
+
this.search.start();
|
|
971
953
|
this.tui.requestRender();
|
|
972
954
|
}
|
|
973
955
|
|
|
974
956
|
private handleSearchInput(data: string): void {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
this.tui.requestRender();
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (matchesKey(data, "enter")) {
|
|
983
|
-
const query = this.draftSearchQuery.trim();
|
|
984
|
-
this.searchMode = false;
|
|
985
|
-
this.draftSearchQuery = "";
|
|
986
|
-
this.searchQuery = query;
|
|
987
|
-
this.searchMessage = "";
|
|
988
|
-
if (query) {
|
|
989
|
-
this.jumpSearch(1);
|
|
990
|
-
} else {
|
|
991
|
-
this.tui.requestRender();
|
|
992
|
-
}
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (matchesKey(data, "ctrl+u")) {
|
|
997
|
-
this.draftSearchQuery = "";
|
|
998
|
-
this.tui.requestRender();
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
if (matchesKey(data, "backspace") || data === "\u007f" || data === "\b") {
|
|
1003
|
-
this.draftSearchQuery = this.draftSearchQuery.slice(0, -1);
|
|
1004
|
-
this.tui.requestRender();
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
if (data.length === 1 && data >= " " && data !== "\u007f") {
|
|
1009
|
-
this.draftSearchQuery += data;
|
|
1010
|
-
this.tui.requestRender();
|
|
1011
|
-
}
|
|
957
|
+
const result = this.search.handleInput(data, this.selected);
|
|
958
|
+
if (result.selected != null) this.selected = result.selected;
|
|
959
|
+
this.tui.requestRender();
|
|
1012
960
|
}
|
|
1013
961
|
|
|
1014
962
|
private clearSearch(): void {
|
|
1015
|
-
this.
|
|
1016
|
-
this.searchQuery = "";
|
|
1017
|
-
this.draftSearchQuery = "";
|
|
1018
|
-
this.searchMessage = "";
|
|
963
|
+
this.search.clear();
|
|
1019
964
|
this.tui.requestRender();
|
|
1020
965
|
}
|
|
1021
966
|
|
|
1022
967
|
private jumpSearch(direction: 1 | -1): void {
|
|
1023
|
-
const
|
|
1024
|
-
if (
|
|
1025
|
-
|
|
1026
|
-
const matches = this.getSearchMatchIndexes(query);
|
|
1027
|
-
if (matches.length === 0) {
|
|
1028
|
-
this.searchMessage = `No matches for /${query}`;
|
|
1029
|
-
this.tui.requestRender();
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
this.searchMessage = "";
|
|
1034
|
-
const current =
|
|
1035
|
-
direction === 1
|
|
1036
|
-
? matches.find((index) => index > this.selected)
|
|
1037
|
-
: [...matches].reverse().find((index) => index < this.selected);
|
|
1038
|
-
this.selected =
|
|
1039
|
-
current ?? (direction === 1 ? matches[0]! : matches[matches.length - 1]!);
|
|
968
|
+
const result = this.search.jump(direction, this.selected);
|
|
969
|
+
if (result.selected != null) this.selected = result.selected;
|
|
1040
970
|
this.tui.requestRender();
|
|
1041
971
|
}
|
|
1042
972
|
|
|
1043
|
-
private getSearchMatchIndexes(query: string): number[] {
|
|
1044
|
-
const needle = query.toLocaleLowerCase();
|
|
1045
|
-
if (!needle) return [];
|
|
1046
|
-
|
|
1047
|
-
const matches: number[] = [];
|
|
1048
|
-
this.lines.forEach((line, index) => {
|
|
1049
|
-
if (line.text.toLocaleLowerCase().includes(needle)) matches.push(index);
|
|
1050
|
-
});
|
|
1051
|
-
return matches;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
973
|
private getSplitDiffRows(): SplitDiffRow[] {
|
|
1055
974
|
if (this.splitRows) return this.splitRows;
|
|
1056
975
|
|
|
@@ -1100,11 +1019,11 @@ export class ReviewComponent {
|
|
|
1100
1019
|
}
|
|
1101
1020
|
|
|
1102
1021
|
private getDisplayText(line: ReviewLine): string {
|
|
1103
|
-
|
|
1104
|
-
line.kind === "remove" ||
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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);
|
|
1108
1027
|
}
|
|
1109
1028
|
|
|
1110
1029
|
private applyDiffBackground(
|
|
@@ -1186,3 +1105,19 @@ export class ReviewComponent {
|
|
|
1186
1105
|
return getCurrentHunkScope(this.lines, this.selected);
|
|
1187
1106
|
}
|
|
1188
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
|