pi-readseek 0.3.16 → 0.3.18
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/package.json +2 -4
- package/src/coerce-obvious-int.ts +1 -10
- package/src/diff-data.ts +0 -1
- package/src/edit-diff.ts +13 -4
- package/src/edit-syntax-validate.ts +33 -104
- package/src/edit.ts +52 -66
- package/src/readseek-client.ts +46 -0
- package/src/readseek-settings.ts +0 -12
- package/src/replace-symbol.ts +4 -2
- package/src/write.ts +1 -1
- package/src/readseek/parser-errors.ts +0 -19
- package/src/readseek/parser-loader.ts +0 -78
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "Pi extension for readseek-backed hash-anchored read/edit/grep, structural code maps, structural search, and file exploration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -39,12 +39,10 @@
|
|
|
39
39
|
"node": ">=20.0.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@jarkkojs/readseek": "^0.3.
|
|
42
|
+
"@jarkkojs/readseek": "^0.3.17",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"ignore": "^7.0.5",
|
|
45
45
|
"picomatch": "^4.0.4",
|
|
46
|
-
"tree-sitter-wasms": "0.1.13",
|
|
47
|
-
"web-tree-sitter": "0.25.10",
|
|
48
46
|
"xxhash-wasm": "^1.1.0"
|
|
49
47
|
},
|
|
50
48
|
"peerDependencies": {
|
|
@@ -4,16 +4,7 @@ type CoercedIntResult =
|
|
|
4
4
|
| { ok: true; value: number | undefined }
|
|
5
5
|
| { ok: false; message: string };
|
|
6
6
|
|
|
7
|
-
export function coerceObviousBase10Int(value: unknown):
|
|
8
|
-
export function coerceObviousBase10Int(value: unknown, name: string): CoercedIntResult;
|
|
9
|
-
export function coerceObviousBase10Int(value: unknown, name?: string): unknown | CoercedIntResult {
|
|
10
|
-
if (name === undefined) {
|
|
11
|
-
if (typeof value === "string" && BASE10_INT_RE.test(value)) {
|
|
12
|
-
return Number.parseInt(value, 10);
|
|
13
|
-
}
|
|
14
|
-
return value;
|
|
15
|
-
}
|
|
16
|
-
|
|
7
|
+
export function coerceObviousBase10Int(value: unknown, name: string): CoercedIntResult {
|
|
17
8
|
if (value === undefined) {
|
|
18
9
|
return { ok: true, value: undefined };
|
|
19
10
|
}
|
package/src/diff-data.ts
CHANGED
package/src/edit-diff.ts
CHANGED
|
@@ -4,10 +4,19 @@ import { CONFUSABLE_HYPHENS_RE } from "./confusable-hyphens.js";
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
export function detectLineEnding(content: string): "\r\n" | "\n" {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
let crlf = 0;
|
|
8
|
+
let lf = 0;
|
|
9
|
+
for (let i = 0; i < content.length; i++) {
|
|
10
|
+
if (content[i] === "\r" && i + 1 < content.length && content[i + 1] === "\n") {
|
|
11
|
+
crlf++;
|
|
12
|
+
i++;
|
|
13
|
+
} else if (content[i] === "\n") {
|
|
14
|
+
lf++;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (lf === 0) return "\r\n";
|
|
18
|
+
if (crlf === 0) return "\n";
|
|
19
|
+
return crlf > lf ? "\r\n" : "\n";
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export function normalizeToLF(text: string): string {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getWasmParser, type WasmLanguageId } from "./readseek/parser-loader.js";
|
|
3
|
-
import { reportParserError } from "./readseek/parser-errors.js";
|
|
1
|
+
import { readseekCheck, type ReadseekCheckOutput, type ReadseekDiagnostic } from "./readseek-client.js";
|
|
4
2
|
|
|
5
3
|
export interface ValidateInput {
|
|
6
4
|
filePath: string;
|
|
@@ -14,122 +12,53 @@ export interface ValidateResult {
|
|
|
14
12
|
newMissingCount: number;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
errors: Array<{ startLine: number; endLine: number }>;
|
|
19
|
-
missing: Array<{ startLine: number; endLine: number }>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function countNodes(parser: WasmParser, source: string): NodeStats {
|
|
23
|
-
const tree: Tree | null = parser.parse(source);
|
|
24
|
-
const errors: Array<{ startLine: number; endLine: number }> = [];
|
|
25
|
-
const missing: Array<{ startLine: number; endLine: number }> = [];
|
|
26
|
-
if (!tree) return { errors, missing };
|
|
27
|
-
try {
|
|
28
|
-
const stack: SyntaxNode[] = [tree.rootNode];
|
|
29
|
-
while (stack.length > 0) {
|
|
30
|
-
const node = stack.pop()!;
|
|
31
|
-
if (node.type === "ERROR") {
|
|
32
|
-
errors.push({
|
|
33
|
-
startLine: node.startPosition.row + 1,
|
|
34
|
-
endLine: node.endPosition.row + 1,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
if (node.isMissing) {
|
|
38
|
-
missing.push({
|
|
39
|
-
startLine: node.startPosition.row + 1,
|
|
40
|
-
endLine: node.endPosition.row + 1,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
for (let i = 0; i < node.namedChildCount; i++) {
|
|
44
|
-
const c = node.namedChild(i);
|
|
45
|
-
if (c) stack.push(c);
|
|
46
|
-
}
|
|
47
|
-
// Also descend into anonymous children to find MISSING tokens.
|
|
48
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
49
|
-
const c = node.child(i);
|
|
50
|
-
if (c && !c.isNamed) stack.push(c);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return { errors, missing };
|
|
54
|
-
} finally {
|
|
55
|
-
tree.delete();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function dedupeSortLines(
|
|
60
|
-
ranges: Array<{ startLine: number; endLine: number }>,
|
|
61
|
-
): string[] {
|
|
15
|
+
function dedupeSortLines(diagnostics: ReadseekDiagnostic[]): string[] {
|
|
62
16
|
const seen = new Set<string>();
|
|
63
17
|
const out: Array<{ key: string; start: number }> = [];
|
|
64
|
-
for (const
|
|
65
|
-
const key =
|
|
66
|
-
|
|
67
|
-
|
|
18
|
+
for (const diagnostic of diagnostics) {
|
|
19
|
+
const key =
|
|
20
|
+
diagnostic.start_line === diagnostic.end_line
|
|
21
|
+
? String(diagnostic.start_line)
|
|
22
|
+
: `${diagnostic.start_line}-${diagnostic.end_line}`;
|
|
68
23
|
if (!seen.has(key)) {
|
|
69
24
|
seen.add(key);
|
|
70
|
-
out.push({ key, start:
|
|
25
|
+
out.push({ key, start: diagnostic.start_line });
|
|
71
26
|
}
|
|
72
27
|
}
|
|
73
28
|
out.sort((a, b) => a.start - b.start);
|
|
74
29
|
return out.map((o) => o.key);
|
|
75
30
|
}
|
|
76
31
|
|
|
77
|
-
const
|
|
78
|
-
".rs": "rust",
|
|
79
|
-
".c": "cpp",
|
|
80
|
-
".cc": "cpp",
|
|
81
|
-
".cpp": "cpp",
|
|
82
|
-
".cxx": "cpp",
|
|
83
|
-
".h": "c-header",
|
|
84
|
-
".hpp": "c-header",
|
|
85
|
-
".hxx": "c-header",
|
|
86
|
-
".java": "java",
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
function detectWasmLang(filePath: string): WasmLanguageId | null {
|
|
90
|
-
for (const [ext, langId] of Object.entries(EXTENSION_TO_LANGUAGE)) {
|
|
91
|
-
if (filePath.toLowerCase().endsWith(ext)) return langId;
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
32
|
+
const EMPTY: ReadseekCheckOutput = { errorCount: 0, missingCount: 0, diagnostics: [] };
|
|
95
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Compare parse diagnostics between `before` and `after` and report any newly
|
|
36
|
+
* introduced syntax errors. Uses readseek's native `check`, so every language
|
|
37
|
+
* with a tree-sitter parser is validated.
|
|
38
|
+
*
|
|
39
|
+
* Returns `null` when nothing new appears or when readseek cannot parse the
|
|
40
|
+
* file, so a validator failure never blocks an edit.
|
|
41
|
+
*/
|
|
96
42
|
export async function validateSyntaxRegression(
|
|
97
|
-
|
|
43
|
+
input: ValidateInput,
|
|
98
44
|
): Promise<ValidateResult | null> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const parser = await getWasmParser(langId);
|
|
102
|
-
if (!parser) return null;
|
|
103
|
-
|
|
45
|
+
let before: ReadseekCheckOutput;
|
|
46
|
+
let after: ReadseekCheckOutput;
|
|
104
47
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
48
|
+
before = input.before === undefined ? EMPTY : await readseekCheck(input.filePath, input.before);
|
|
49
|
+
after = await readseekCheck(input.filePath, input.after);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
109
53
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
0,
|
|
113
|
-
afterStats.errors.length - beforeStats.errors.length - 1,
|
|
114
|
-
);
|
|
115
|
-
const newMissingCount = Math.max(
|
|
116
|
-
0,
|
|
117
|
-
afterStats.missing.length - beforeStats.missing.length,
|
|
118
|
-
);
|
|
54
|
+
const newErrorCount = Math.max(0, after.errorCount - before.errorCount);
|
|
55
|
+
const newMissingCount = Math.max(0, after.missingCount - before.missingCount);
|
|
119
56
|
|
|
120
|
-
|
|
57
|
+
if (newErrorCount === 0 && newMissingCount === 0) return null;
|
|
121
58
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
} catch (err) {
|
|
128
|
-
reportParserError(`wasm:syntax-validate:${langId}:${err instanceof Error ? err.message : String(err)}`, err, {
|
|
129
|
-
context: `tree-sitter syntax validation failed for ${langId}`,
|
|
130
|
-
});
|
|
131
|
-
return null;
|
|
132
|
-
} finally {
|
|
133
|
-
parser.delete();
|
|
134
|
-
}
|
|
59
|
+
return {
|
|
60
|
+
errorLines: dedupeSortLines(after.diagnostics),
|
|
61
|
+
newErrorCount,
|
|
62
|
+
newMissingCount,
|
|
63
|
+
};
|
|
135
64
|
}
|
package/src/edit.ts
CHANGED
|
@@ -116,6 +116,26 @@ function buildEditError(
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function mapEditFileError(err: any, filePath: string, displayPath: string, phase: "read" | "write"): ReturnType<typeof buildEditError> {
|
|
120
|
+
const code = err?.code;
|
|
121
|
+
if (code === "EISDIR") {
|
|
122
|
+
return buildEditError(filePath, "path-is-directory",
|
|
123
|
+
`Path is a directory: ${displayPath}`,
|
|
124
|
+
`Use ls(${JSON.stringify(displayPath)}) to inspect directories.`);
|
|
125
|
+
}
|
|
126
|
+
if (code === "ENOENT") {
|
|
127
|
+
return buildEditError(filePath, "file-not-found",
|
|
128
|
+
`${phase === "write" ? "Failed to write file" : "File not found"}: ${displayPath}`);
|
|
129
|
+
}
|
|
130
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
131
|
+
return buildEditError(filePath, "permission-denied", `Permission denied: ${displayPath}`);
|
|
132
|
+
}
|
|
133
|
+
const prefix = phase === "write" ? "Failed to write file" : "File not readable";
|
|
134
|
+
const message = `${prefix}: ${displayPath}${err?.message ? ` — ${err.message}` : ""}`;
|
|
135
|
+
return buildEditError(filePath, "fs-error", message, undefined,
|
|
136
|
+
{ fsCode: code, fsMessage: err?.message });
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
export interface EditToolOptions {
|
|
120
140
|
wasReadInSession?: (absolutePath: string) => boolean;
|
|
121
141
|
syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
|
|
@@ -227,27 +247,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
227
247
|
try {
|
|
228
248
|
rawBuffer = await fsReadFile(absolutePath);
|
|
229
249
|
} catch (err: any) {
|
|
230
|
-
|
|
231
|
-
let errCode: string;
|
|
232
|
-
let message: string;
|
|
233
|
-
let hint: string | undefined;
|
|
234
|
-
let errorDetails: { fsCode?: string; fsMessage?: string } | undefined;
|
|
235
|
-
if (code === "EISDIR") {
|
|
236
|
-
errCode = "path-is-directory";
|
|
237
|
-
message = `Path is a directory: ${path}`;
|
|
238
|
-
hint = `Use ls(${JSON.stringify(path)}) to inspect directories.`;
|
|
239
|
-
} else if (code === "ENOENT") {
|
|
240
|
-
errCode = "file-not-found";
|
|
241
|
-
message = `File not found: ${path}`;
|
|
242
|
-
} else if (code === "EACCES" || code === "EPERM") {
|
|
243
|
-
errCode = "permission-denied";
|
|
244
|
-
message = `Permission denied: ${path}`;
|
|
245
|
-
} else {
|
|
246
|
-
errCode = "fs-error";
|
|
247
|
-
message = `File not readable: ${path}${err?.message ? ` — ${err.message}` : ""}`;
|
|
248
|
-
errorDetails = { fsCode: code, fsMessage: err?.message };
|
|
249
|
-
}
|
|
250
|
-
return buildEditError(absolutePath, errCode, message, hint, errorDetails);
|
|
250
|
+
return mapEditFileError(err, absolutePath, path, "read");
|
|
251
251
|
}
|
|
252
252
|
if (isBinaryBuffer(rawBuffer)) {
|
|
253
253
|
const message = `Cannot edit binary file: ${path}`;
|
|
@@ -258,6 +258,27 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
258
258
|
const { bom, text: content } = stripBom(raw);
|
|
259
259
|
const originalEnding = detectLineEnding(content);
|
|
260
260
|
const originalNormalized = normalizeToLF(content);
|
|
261
|
+
const origLines = originalNormalized.split("\n");
|
|
262
|
+
const anchorSnapshots = new Map<string, string>();
|
|
263
|
+
for (const edit of anchorEdits) {
|
|
264
|
+
const refs: string[] = [];
|
|
265
|
+
if ("set_line" in edit) refs.push(edit.set_line.anchor);
|
|
266
|
+
else if ("replace_lines" in edit) {
|
|
267
|
+
refs.push(edit.replace_lines.start_anchor, edit.replace_lines.end_anchor);
|
|
268
|
+
} else if ("insert_after" in edit) refs.push(edit.insert_after.anchor);
|
|
269
|
+
for (const ref of refs) {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = parseLineRef(ref);
|
|
272
|
+
if (parsed.line >= 1 && parsed.line <= origLines.length) {
|
|
273
|
+
const lineContent = origLines[parsed.line - 1];
|
|
274
|
+
const hash = computeLineHash(parsed.line, lineContent);
|
|
275
|
+
anchorSnapshots.set(ref, `${parsed.line}:${hash}|${escapeControlCharsForDisplay(lineContent)}`);
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
/* skip malformed refs */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
261
282
|
let preAnchorContent = originalNormalized;
|
|
262
283
|
// AC 26: reject anchored edits that target a line inside any replace_symbol
|
|
263
284
|
// pre-replace range. Resolve each target against the ORIGINAL content so the
|
|
@@ -302,8 +323,8 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
302
323
|
let startLine: number | undefined;
|
|
303
324
|
let endLine: number | undefined;
|
|
304
325
|
try {
|
|
305
|
-
startLine = parseLineRef(
|
|
306
|
-
endLine = parseLineRef(
|
|
326
|
+
startLine = parseLineRef(edit.replace_lines.start_anchor).line;
|
|
327
|
+
endLine = parseLineRef(edit.replace_lines.end_anchor).line;
|
|
307
328
|
} catch {
|
|
308
329
|
// Let the normal anchored edit validation report malformed anchors later.
|
|
309
330
|
}
|
|
@@ -319,10 +340,10 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
319
340
|
}
|
|
320
341
|
}
|
|
321
342
|
const refs: string[] = [];
|
|
322
|
-
if ("set_line" in edit) refs.push(
|
|
343
|
+
if ("set_line" in edit) refs.push(edit.set_line.anchor);
|
|
323
344
|
else if ("replace_lines" in edit) {
|
|
324
|
-
refs.push(
|
|
325
|
-
} else if ("insert_after" in edit) refs.push(
|
|
345
|
+
refs.push(edit.replace_lines.start_anchor, edit.replace_lines.end_anchor);
|
|
346
|
+
} else if ("insert_after" in edit) refs.push(edit.insert_after.anchor);
|
|
326
347
|
for (const ref of refs) {
|
|
327
348
|
let parsedLine: number | undefined;
|
|
328
349
|
try {
|
|
@@ -412,30 +433,9 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
412
433
|
.join("\n");
|
|
413
434
|
diagnostic += "\nRe-read the file to see the current state.";
|
|
414
435
|
} else {
|
|
415
|
-
|
|
416
|
-
const lines = result.split("\n");
|
|
417
|
-
const targetLines: string[] = [];
|
|
418
|
-
for (const edit of edits) {
|
|
419
|
-
const refs: string[] = [];
|
|
420
|
-
if ("set_line" in edit) refs.push((edit as any).set_line.anchor);
|
|
421
|
-
else if ("replace_lines" in edit) {
|
|
422
|
-
refs.push((edit as any).replace_lines.start_anchor, (edit as any).replace_lines.end_anchor);
|
|
423
|
-
} else if ("insert_after" in edit) refs.push((edit as any).insert_after.anchor);
|
|
424
|
-
for (const ref of refs) {
|
|
425
|
-
try {
|
|
426
|
-
const parsed = parseLineRef(ref);
|
|
427
|
-
if (parsed.line >= 1 && parsed.line <= lines.length) {
|
|
428
|
-
const lineContent = lines[parsed.line - 1];
|
|
429
|
-
const hash = computeLineHash(parsed.line, lineContent);
|
|
430
|
-
targetLines.push(`${parsed.line}:${hash}|${escapeControlCharsForDisplay(lineContent)}`);
|
|
431
|
-
}
|
|
432
|
-
} catch {
|
|
433
|
-
/* skip malformed refs */
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
436
|
+
const targetLines = [...new Set(anchorSnapshots.values())];
|
|
437
437
|
if (targetLines.length > 0) {
|
|
438
|
-
const preview =
|
|
438
|
+
const preview = targetLines.slice(0, 5).join("\n");
|
|
439
439
|
diagnostic += `\nThe file currently contains:\n${preview}\nYour edits were normalized back to the original content. Ensure your replacement changes actual code, not just formatting.`;
|
|
440
440
|
}
|
|
441
441
|
}
|
|
@@ -467,18 +467,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
467
467
|
try {
|
|
468
468
|
await fsWriteFile(absolutePath, writeContent, "utf-8");
|
|
469
469
|
} catch (err: any) {
|
|
470
|
-
|
|
471
|
-
const code =
|
|
472
|
-
err?.code === "EACCES" || err?.code === "EPERM"
|
|
473
|
-
? "permission-denied"
|
|
474
|
-
: err?.code === "ENOENT"
|
|
475
|
-
? "file-not-found"
|
|
476
|
-
: "fs-error";
|
|
477
|
-
const message =
|
|
478
|
-
code === "fs-error" && err?.message ? `${wrapped.message} — ${err.message}` : wrapped.message;
|
|
479
|
-
return buildEditError(absolutePath, code, message, undefined, code === "fs-error"
|
|
480
|
-
? { fsCode: err?.code, fsMessage: err?.message }
|
|
481
|
-
: undefined);
|
|
470
|
+
return mapEditFileError(err, absolutePath, path, "write");
|
|
482
471
|
}
|
|
483
472
|
|
|
484
473
|
if (input.postEditVerify === true) {
|
|
@@ -555,7 +544,6 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
555
544
|
semanticSummary,
|
|
556
545
|
});
|
|
557
546
|
|
|
558
|
-
const warn = warnings.length ? `\n\nWarnings:\n${warnings.join("\n")}` : "";
|
|
559
547
|
return {
|
|
560
548
|
content: [{ type: "text", text: builtOutput.text }],
|
|
561
549
|
details: {
|
|
@@ -581,10 +569,8 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
581
569
|
};
|
|
582
570
|
});
|
|
583
571
|
} catch (err: any) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const message = `File not readable: ${path}${err?.message ? ` — ${err.message}` : ""}`;
|
|
587
|
-
return buildEditError(absolutePath, "fs-error", message, undefined, { fsCode: code, fsMessage: err?.message });
|
|
572
|
+
if (typeof err?.code === "string") {
|
|
573
|
+
return mapEditFileError(err, absolutePath, path, "read");
|
|
588
574
|
}
|
|
589
575
|
throw err;
|
|
590
576
|
}
|
package/src/readseek-client.ts
CHANGED
|
@@ -94,6 +94,18 @@ export interface ReadseekRefsOptions {
|
|
|
94
94
|
signal?: AbortSignal;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export interface ReadseekDiagnostic {
|
|
98
|
+
kind: "error" | "missing";
|
|
99
|
+
start_line: number;
|
|
100
|
+
end_line: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ReadseekCheckOutput {
|
|
104
|
+
errorCount: number;
|
|
105
|
+
missingCount: number;
|
|
106
|
+
diagnostics: ReadseekDiagnostic[];
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
export interface ReadseekSearchOptions {
|
|
98
110
|
language?: string;
|
|
99
111
|
cached?: boolean;
|
|
@@ -472,3 +484,37 @@ export async function readseekRefs(
|
|
|
472
484
|
if (options.ignored) args.push("--ignored");
|
|
473
485
|
return parseRefsOutput(await runReadseek(args, { signal: options.signal })).references;
|
|
474
486
|
}
|
|
487
|
+
|
|
488
|
+
function parseDiagnosticKind(value: unknown): ReadseekDiagnostic["kind"] {
|
|
489
|
+
if (value === "error" || value === "missing") return value;
|
|
490
|
+
throw new Error("invalid readseek diagnostic.kind");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function parseCheckOutput(value: unknown): ReadseekCheckOutput {
|
|
494
|
+
if (!value || typeof value !== "object") throw new Error("invalid readseek check output");
|
|
495
|
+
const output = value as Record<string, unknown>;
|
|
496
|
+
if (!Array.isArray(output.diagnostics)) throw new Error("invalid readseek diagnostics");
|
|
497
|
+
return {
|
|
498
|
+
errorCount: requireNumber(output.error_count, "error_count"),
|
|
499
|
+
missingCount: requireNumber(output.missing_count, "missing_count"),
|
|
500
|
+
diagnostics: output.diagnostics.map((diagnostic) => {
|
|
501
|
+
if (!diagnostic || typeof diagnostic !== "object") throw new Error("invalid readseek diagnostic");
|
|
502
|
+
const item = diagnostic as Record<string, unknown>;
|
|
503
|
+
return {
|
|
504
|
+
kind: parseDiagnosticKind(item.kind),
|
|
505
|
+
start_line: requireNumber(item.start_line, "diagnostic.start_line"),
|
|
506
|
+
end_line: requireNumber(item.end_line, "diagnostic.end_line"),
|
|
507
|
+
};
|
|
508
|
+
}),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export async function readseekCheck(
|
|
513
|
+
filePath: string,
|
|
514
|
+
content: string,
|
|
515
|
+
options: { signal?: AbortSignal } = {},
|
|
516
|
+
): Promise<ReadseekCheckOutput> {
|
|
517
|
+
return parseCheckOutput(
|
|
518
|
+
await runReadseek(["check", "--stdin", filePath], { signal: options.signal, stdin: content }),
|
|
519
|
+
);
|
|
520
|
+
}
|
package/src/readseek-settings.ts
CHANGED
|
@@ -49,18 +49,6 @@ function readPositive(
|
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function readBoolean(
|
|
53
|
-
raw: Record<string, unknown>,
|
|
54
|
-
key: string,
|
|
55
|
-
path: string,
|
|
56
|
-
source: string,
|
|
57
|
-
warnings: ReadseekSettingsWarning[],
|
|
58
|
-
): boolean | undefined {
|
|
59
|
-
if (!(key in raw)) return undefined;
|
|
60
|
-
if (typeof raw[key] === "boolean") return raw[key];
|
|
61
|
-
warnings.push(invalid(source, path));
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
52
|
|
|
65
53
|
function validateSettings(raw: unknown, source: string): ReadseekSettingsResult {
|
|
66
54
|
const settings: ReadseekJsonSettings = {};
|
package/src/replace-symbol.ts
CHANGED
|
@@ -53,9 +53,11 @@ export async function replaceSymbol(input: ReplaceSymbolInput): Promise<ReplaceS
|
|
|
53
53
|
const reindented = reindent(dedent(input.newBody), indent);
|
|
54
54
|
const warnings: string[] = [];
|
|
55
55
|
const leaf = input.symbol.replace(/@\d+$/, "").split(".").pop() ?? "";
|
|
56
|
+
const declNameRe =
|
|
57
|
+
/\b(?:(?:export\s+(?:default\s+)?|async\s+|public\s+|private\s+|protected\s+|static\s+)*)(?:function\*?|class|const|let|var|fn|def|method|type|interface|enum)\s+([A-Za-z_$][\w$]*)/s;
|
|
56
58
|
const firstDeclName =
|
|
57
|
-
reindented.match(
|
|
58
|
-
?? reindented.match(/^\s*(?:[\w$<>,?\s]+\s+)?([A-Za-z_$][\w$]*)\s*\(/)?.[1];
|
|
59
|
+
reindented.match(declNameRe)?.[1]
|
|
60
|
+
?? reindented.match(/^\s*(?:(?:async\s+)?[\w$<>,?\s]+\s+)?([A-Za-z_$][\w$]*)\s*\(/)?.[1];
|
|
59
61
|
if (leaf && firstDeclName && firstDeclName !== leaf) {
|
|
60
62
|
warnings.push(`name-mismatch: expected ${leaf}, got ${firstDeclName}`);
|
|
61
63
|
}
|
package/src/write.ts
CHANGED
|
@@ -294,7 +294,7 @@ export async function executeWrite(opts: {
|
|
|
294
294
|
warnings: readseekWarnings,
|
|
295
295
|
diff: diffResult.diff,
|
|
296
296
|
diffData,
|
|
297
|
-
...(requestMap
|
|
297
|
+
...(requestMap ? { map: { appended: mapAppended } } : {}),
|
|
298
298
|
},
|
|
299
299
|
};
|
|
300
300
|
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
const emitted = new Set<string>();
|
|
2
|
-
|
|
3
|
-
function causeMessage(err: unknown): string {
|
|
4
|
-
if (err instanceof Error) return err.message;
|
|
5
|
-
return String(err);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function reportParserError(
|
|
9
|
-
onceKey: string,
|
|
10
|
-
err: unknown,
|
|
11
|
-
options: { context?: string } = {},
|
|
12
|
-
): void {
|
|
13
|
-
if (!process.env.PI_READSEEK_DEBUG) return;
|
|
14
|
-
if (emitted.has(onceKey)) return;
|
|
15
|
-
emitted.add(onceKey);
|
|
16
|
-
const context = options.context ?? onceKey;
|
|
17
|
-
console.error(`[readseek] ${context}: ${causeMessage(err)}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { Language, Parser } from "web-tree-sitter";
|
|
4
|
-
import { reportParserError } from "./parser-errors.js";
|
|
5
|
-
|
|
6
|
-
export type WasmLanguageId = "rust" | "cpp" | "c-header" | "java";
|
|
7
|
-
export type WasmParser = Parser;
|
|
8
|
-
|
|
9
|
-
const require_ = createRequire(import.meta.url);
|
|
10
|
-
const wasmNames: Record<WasmLanguageId, string> = {
|
|
11
|
-
rust: "rust",
|
|
12
|
-
cpp: "cpp",
|
|
13
|
-
"c-header": "cpp",
|
|
14
|
-
java: "java",
|
|
15
|
-
};
|
|
16
|
-
let initPromise: Promise<void> | null = null;
|
|
17
|
-
const languages = new Map<WasmLanguageId, Language>();
|
|
18
|
-
const languageLoads = new Map<WasmLanguageId, Promise<Language | null>>();
|
|
19
|
-
|
|
20
|
-
function isBun(): boolean {
|
|
21
|
-
return typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function wasmPath(langId: WasmLanguageId): string {
|
|
25
|
-
const pkg = require_.resolve("tree-sitter-wasms/package.json");
|
|
26
|
-
return join(dirname(pkg), "out", `tree-sitter-${wasmNames[langId]}.wasm`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function init(): Promise<void> {
|
|
30
|
-
initPromise ??= Parser.init().catch((err: unknown) => {
|
|
31
|
-
initPromise = null;
|
|
32
|
-
reportParserError("wasm:init", err, { context: "web-tree-sitter initialization failed" });
|
|
33
|
-
throw err;
|
|
34
|
-
});
|
|
35
|
-
return initPromise;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function language(langId: WasmLanguageId): Promise<Language | null> {
|
|
39
|
-
const loaded = languages.get(langId);
|
|
40
|
-
if (loaded) return loaded;
|
|
41
|
-
|
|
42
|
-
const inFlight = languageLoads.get(langId);
|
|
43
|
-
if (inFlight) return inFlight;
|
|
44
|
-
|
|
45
|
-
const loadPromise = (async () => {
|
|
46
|
-
try {
|
|
47
|
-
await init();
|
|
48
|
-
const lang = await Language.load(wasmPath(langId));
|
|
49
|
-
languages.set(langId, lang);
|
|
50
|
-
return lang;
|
|
51
|
-
} catch (err) {
|
|
52
|
-
reportParserError(`wasm:load:${langId}`, err, {
|
|
53
|
-
context: `tree-sitter WASM grammar load failed for ${langId}`,
|
|
54
|
-
});
|
|
55
|
-
return null;
|
|
56
|
-
} finally {
|
|
57
|
-
languageLoads.delete(langId);
|
|
58
|
-
}
|
|
59
|
-
})();
|
|
60
|
-
|
|
61
|
-
languageLoads.set(langId, loadPromise);
|
|
62
|
-
return loadPromise;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function getWasmParser(langId: WasmLanguageId): Promise<WasmParser | null> {
|
|
66
|
-
if (isBun()) return null;
|
|
67
|
-
const lang = await language(langId);
|
|
68
|
-
if (!lang) return null;
|
|
69
|
-
try {
|
|
70
|
-
const parser = new Parser();
|
|
71
|
-
parser.setLanguage(lang);
|
|
72
|
-
return parser;
|
|
73
|
-
} catch (err) {
|
|
74
|
-
reportParserError(`wasm:parser:${langId}`, err, { context: `tree-sitter WASM parser creation failed for ${langId}` });
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|