pi-readseek 0.3.16 → 0.3.17
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/edit-syntax-validate.ts +33 -104
- package/src/readseek-client.ts +46 -0
- 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.17",
|
|
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": {
|
|
@@ -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 - 1);
|
|
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/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
|
+
}
|
|
@@ -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
|
-
|