pi-diff-review 0.1.19 → 0.1.21
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 +13 -2
- package/extensions/review.ts +5 -1
- package/package.json +1 -1
- package/src/diff/source.ts +6 -41
- package/src/explanation/controller.ts +34 -1
- package/src/explanation/explainer.ts +22 -2
- package/src/index.ts +170 -226
- package/src/review/cache.ts +171 -0
- package/src/review/component.ts +85 -14
- package/src/review/prompt.ts +25 -2
- package/src/review/search.ts +83 -20
- package/src/review/types.ts +8 -0
- package/src/review/workspace-comments.ts +493 -0
- package/src/shared/args.ts +44 -0
- package/src/view/parser.ts +54 -0
- package/src/view/source.ts +132 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, resolve } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { tokenizeShellArgs } from "../shared/args.ts";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
|
|
7
|
+
const IGNORED_DIRS = new Set([".git", "node_modules"]);
|
|
8
|
+
|
|
9
|
+
export type ViewSource = {
|
|
10
|
+
label: string;
|
|
11
|
+
promptLabel: string;
|
|
12
|
+
paths: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function parseViewSource(args: string): ViewSource {
|
|
16
|
+
const trimmed = args.trim();
|
|
17
|
+
if (!trimmed) {
|
|
18
|
+
throw new Error("Provide one or more files or folders to /view.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let paths: string[];
|
|
22
|
+
try {
|
|
23
|
+
paths = tokenizeShellArgs(trimmed);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
throw new Error(message.replace(/in arguments$/, "in /view paths"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalizedPaths = paths.map(stripPiPathPrefix);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
label: `/view ${trimmed}`,
|
|
33
|
+
promptLabel: `the selected code from /view ${trimmed}`,
|
|
34
|
+
paths: normalizedPaths,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveViewFiles(cwd: string, source: ViewSource): string[] {
|
|
39
|
+
const gitFilesByDir = new Map<string, string[]>();
|
|
40
|
+
const resolved = new Set<string>();
|
|
41
|
+
|
|
42
|
+
for (const inputPath of source.paths) {
|
|
43
|
+
const absolutePath = resolve(cwd, inputPath);
|
|
44
|
+
const stats = statSync(absolutePath, { throwIfNoEntry: false });
|
|
45
|
+
if (!stats) throw new Error(`Path does not exist: ${inputPath}`);
|
|
46
|
+
|
|
47
|
+
if (stats.isDirectory()) {
|
|
48
|
+
const gitFiles = getGitTrackedAndUntrackedFiles(
|
|
49
|
+
cwd,
|
|
50
|
+
inputPath,
|
|
51
|
+
gitFilesByDir,
|
|
52
|
+
);
|
|
53
|
+
if (gitFiles) {
|
|
54
|
+
for (const file of gitFiles) resolved.add(resolve(cwd, file));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const file of walkDirectory(absolutePath)) {
|
|
59
|
+
resolved.add(file);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (stats.isFile()) {
|
|
65
|
+
resolved.add(absolutePath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const files = [...resolved].filter((path) => isViewableTextFile(path));
|
|
70
|
+
files.sort((left, right) => left.localeCompare(right));
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getGitTrackedAndUntrackedFiles(
|
|
75
|
+
cwd: string,
|
|
76
|
+
dir: string,
|
|
77
|
+
cache: Map<string, string[]>,
|
|
78
|
+
): string[] | undefined {
|
|
79
|
+
const cached = cache.get(dir);
|
|
80
|
+
if (cached) return cached;
|
|
81
|
+
|
|
82
|
+
const result = spawnSync(
|
|
83
|
+
"git",
|
|
84
|
+
["ls-files", "--cached", "--others", "--exclude-standard", "--", dir],
|
|
85
|
+
{
|
|
86
|
+
cwd,
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
if (result.status !== 0) return undefined;
|
|
92
|
+
|
|
93
|
+
const files = result.stdout
|
|
94
|
+
.split("\n")
|
|
95
|
+
.map((line) => line.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
cache.set(dir, files);
|
|
98
|
+
return files;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function* walkDirectory(dir: string): Generator<string> {
|
|
102
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
105
|
+
const entryPath = resolve(dir, entry.name);
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
yield* walkDirectory(entryPath);
|
|
108
|
+
} else if (entry.isFile()) {
|
|
109
|
+
yield entryPath;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function stripPiPathPrefix(path: string): string {
|
|
115
|
+
return path.startsWith("@") ? path.slice(1) : path;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isViewableTextFile(path: string): boolean {
|
|
119
|
+
const stats = statSync(path, { throwIfNoEntry: false });
|
|
120
|
+
if (!stats?.isFile()) return false;
|
|
121
|
+
if (stats.size > DEFAULT_MAX_FILE_BYTES) return false;
|
|
122
|
+
if (extname(path).toLowerCase() === ".png") return false;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const sample = readFileSync(path);
|
|
126
|
+
if (sample.includes(0)) return false;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return true;
|
|
132
|
+
}
|