@mrclrchtr/supi-tree-sitter 10.0.0 → 11.0.0
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 +63 -17
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/LICENSE +21 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/README.md +743 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/index.js +17 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/constants.js +184 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/parse.js +1425 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/picomatch.js +361 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/scan.js +403 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/utils.js +72 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/package.json +83 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/posix.js +3 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +4 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +11 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/file-associations.ts +190 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +22 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/package.json +3 -3
- package/src/language.ts +16 -10
- package/src/operation-support.ts +13 -6
- package/src/provider/tree-sitter-provider.ts +19 -1
- package/src/session/runtime-controller.ts +18 -4
- package/src/session/session.ts +10 -3
- package/src/session/structural-worker-client.ts +17 -2
- package/src/session/structural-worker-process.ts +22 -5
- package/src/session/structural-worker-protocol.ts +36 -1
- package/src/syntax-errors.ts +73 -0
- package/src/syntax-node.ts +4 -0
- package/src/tool/call-sites.ts +15 -9
- package/src/tool/callees.ts +24 -15
- package/src/tool/exports.ts +8 -1
- package/src/tool/imports.ts +233 -7
- package/src/tool/node-at.ts +7 -3
- package/src/tool/outline-expansion.ts +78 -0
- package/src/tool/outline-jvm.ts +25 -4
- package/src/tool/outline-nested.ts +390 -0
- package/src/tool/outline.ts +49 -28
- package/src/types.ts +16 -0
- package/src/worker/process-host.mjs +61 -24
- package/src/worker/runtime.ts +44 -10
- package/src/worker/service.ts +13 -9
- package/src/worker/worker-main.ts +15 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import picomatch from "picomatch";
|
|
3
|
+
|
|
4
|
+
/** Language selected for a C-family source file. */
|
|
5
|
+
export type CFamilyLanguage = "c" | "cpp";
|
|
6
|
+
|
|
7
|
+
/** One ordered, project-relative language rule for an existing C-family file. */
|
|
8
|
+
export interface CFamilyFileAssociationRule {
|
|
9
|
+
readonly pattern: string;
|
|
10
|
+
readonly language: CFamilyLanguage;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** C-family suffixes that already have Tree-sitter and LSP support. */
|
|
14
|
+
export const C_FAMILY_EXTENSIONS = Object.freeze([
|
|
15
|
+
".c",
|
|
16
|
+
".h",
|
|
17
|
+
".cpp",
|
|
18
|
+
".hpp",
|
|
19
|
+
".cc",
|
|
20
|
+
".cxx",
|
|
21
|
+
".hxx",
|
|
22
|
+
".c++",
|
|
23
|
+
".h++",
|
|
24
|
+
] as const);
|
|
25
|
+
|
|
26
|
+
export type CFamilyExtension = (typeof C_FAMILY_EXTENSIONS)[number];
|
|
27
|
+
|
|
28
|
+
const DEFAULT_C_FAMILY_LANGUAGES: Readonly<Record<CFamilyExtension, CFamilyLanguage>> =
|
|
29
|
+
Object.freeze({
|
|
30
|
+
".c": "c",
|
|
31
|
+
".h": "c",
|
|
32
|
+
".cpp": "cpp",
|
|
33
|
+
".hpp": "cpp",
|
|
34
|
+
".cc": "cpp",
|
|
35
|
+
".cxx": "cpp",
|
|
36
|
+
".hxx": "cpp",
|
|
37
|
+
".c++": "cpp",
|
|
38
|
+
".h++": "cpp",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** Immutable C-family language selection for one workspace. */
|
|
42
|
+
export interface CFamilyFileAssociationResolver {
|
|
43
|
+
/** Absolute root used to make project-relative paths. */
|
|
44
|
+
readonly workspaceRoot: string;
|
|
45
|
+
/** Validated ordered rules captured when the workspace host starts. */
|
|
46
|
+
readonly rules: readonly CFamilyFileAssociationRule[];
|
|
47
|
+
/** Select the configured language or the existing suffix default. */
|
|
48
|
+
languageForFile(filePath: string): CFamilyLanguage | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Return the current default language for one supported C-family suffix. */
|
|
52
|
+
export function defaultCFamilyLanguage(extension: string): CFamilyLanguage | undefined {
|
|
53
|
+
return DEFAULT_C_FAMILY_LANGUAGES[extension.toLowerCase() as CFamilyExtension];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate and compile ordered C-family file associations for one workspace.
|
|
58
|
+
* Patterns are case-sensitive globs over POSIX-style, workspace-relative paths.
|
|
59
|
+
* Supported patterns use `*`, `?`, `**`, character classes, and brace alternatives.
|
|
60
|
+
* The first matching rule wins. Rules never expand the eligible suffix set.
|
|
61
|
+
*
|
|
62
|
+
* @throws If the rule list, a rule, a language, or a pattern is invalid.
|
|
63
|
+
*/
|
|
64
|
+
export function createCFamilyFileAssociationResolver(
|
|
65
|
+
workspaceRoot: string,
|
|
66
|
+
input: unknown = [],
|
|
67
|
+
): CFamilyFileAssociationResolver {
|
|
68
|
+
const rules = validateRules(input);
|
|
69
|
+
const matchers = rules.map((rule, index) => ({
|
|
70
|
+
language: rule.language,
|
|
71
|
+
matches: compilePattern(rule.pattern, `code-intelligence.fileAssociations[${index}]`),
|
|
72
|
+
}));
|
|
73
|
+
const root = path.resolve(workspaceRoot);
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
workspaceRoot: root,
|
|
76
|
+
rules,
|
|
77
|
+
languageForFile(filePath: string): CFamilyLanguage | undefined {
|
|
78
|
+
const extension = normalizeExtension(filePath);
|
|
79
|
+
const fallback = defaultCFamilyLanguage(extension);
|
|
80
|
+
if (!fallback) return undefined;
|
|
81
|
+
|
|
82
|
+
const absolutePath = path.resolve(root, filePath);
|
|
83
|
+
const relativePath = toPosixPath(path.relative(root, absolutePath));
|
|
84
|
+
if (
|
|
85
|
+
relativePath === ".." ||
|
|
86
|
+
relativePath.startsWith("../") ||
|
|
87
|
+
path.posix.isAbsolute(relativePath)
|
|
88
|
+
) {
|
|
89
|
+
return fallback;
|
|
90
|
+
}
|
|
91
|
+
for (const rule of matchers) {
|
|
92
|
+
if (rule.matches(relativePath)) return rule.language;
|
|
93
|
+
}
|
|
94
|
+
return fallback;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateRules(input: unknown): readonly CFamilyFileAssociationRule[] {
|
|
100
|
+
if (!Array.isArray(input)) {
|
|
101
|
+
throw new Error('Invalid "code-intelligence.fileAssociations": expected an ordered array.');
|
|
102
|
+
}
|
|
103
|
+
if (input.length > 64) {
|
|
104
|
+
throw new Error('Invalid "code-intelligence.fileAssociations": use no more than 64 rules.');
|
|
105
|
+
}
|
|
106
|
+
const rules: CFamilyFileAssociationRule[] = [];
|
|
107
|
+
const seenPatterns = new Set<string>();
|
|
108
|
+
for (const [index, value] of input.entries()) {
|
|
109
|
+
const key = `code-intelligence.fileAssociations[${index}]`;
|
|
110
|
+
if (!isRecord(value) || !hasExactKeys(value, ["pattern", "language"])) {
|
|
111
|
+
throw new Error(`${key} must contain only "pattern" and "language" fields.`);
|
|
112
|
+
}
|
|
113
|
+
if (typeof value.pattern !== "string") {
|
|
114
|
+
throw new Error(`${key}.pattern must be a string.`);
|
|
115
|
+
}
|
|
116
|
+
if (value.language !== "c" && value.language !== "cpp") {
|
|
117
|
+
throw new Error(`${key}.language must be "c" or "cpp".`);
|
|
118
|
+
}
|
|
119
|
+
if (seenPatterns.has(value.pattern)) {
|
|
120
|
+
throw new Error(`${key}.pattern duplicates an earlier rule.`);
|
|
121
|
+
}
|
|
122
|
+
seenPatterns.add(value.pattern);
|
|
123
|
+
rules.push(Object.freeze({ pattern: value.pattern, language: value.language }));
|
|
124
|
+
}
|
|
125
|
+
return Object.freeze(rules);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function compilePattern(pattern: string, key: string): (file: string) => boolean {
|
|
129
|
+
if (
|
|
130
|
+
pattern.length === 0 ||
|
|
131
|
+
pattern.length > 512 ||
|
|
132
|
+
pattern !== pattern.trim() ||
|
|
133
|
+
pattern.includes("\\") ||
|
|
134
|
+
hasControlCharacter(pattern) ||
|
|
135
|
+
pattern.startsWith("/") ||
|
|
136
|
+
/^[A-Za-z]:/.test(pattern) ||
|
|
137
|
+
pattern.startsWith("!")
|
|
138
|
+
) {
|
|
139
|
+
throw new Error(`${key}.pattern must be a safe workspace-relative glob using "/" separators.`);
|
|
140
|
+
}
|
|
141
|
+
const segments = pattern.split("/");
|
|
142
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
143
|
+
throw new Error(`${key}.pattern contains an unsafe or unsupported path pattern.`);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return picomatch(pattern, {
|
|
147
|
+
nocase: false,
|
|
148
|
+
windows: false,
|
|
149
|
+
dot: true,
|
|
150
|
+
strictBrackets: true,
|
|
151
|
+
nonegate: true,
|
|
152
|
+
noextglob: true,
|
|
153
|
+
maxLength: 512,
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new Error(`${key}.pattern is not a valid glob: ${errorMessage(error)}`, { cause: error });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function hasControlCharacter(value: string): boolean {
|
|
161
|
+
for (const character of value) {
|
|
162
|
+
const code = character.charCodeAt(0);
|
|
163
|
+
if (code < 32 || code === 127) return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeExtension(filePath: string): string {
|
|
169
|
+
return path.posix.extname(toPosixPath(filePath)).toLowerCase();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toPosixPath(value: string): string {
|
|
173
|
+
return value.replaceAll("\\", "/");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
177
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
181
|
+
const allowed = new Set(keys);
|
|
182
|
+
return (
|
|
183
|
+
keys.every((key) => Object.hasOwn(value, key)) &&
|
|
184
|
+
Object.keys(value).every((key) => allowed.has(key))
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function errorMessage(error: unknown): string {
|
|
189
|
+
return error instanceof Error ? error.message : String(error);
|
|
190
|
+
}
|
|
@@ -80,6 +80,14 @@ export interface DocumentCodeSymbol extends CodeSymbol {
|
|
|
80
80
|
*/
|
|
81
81
|
export type CodeResult<T> =
|
|
82
82
|
| { kind: "success"; data: T }
|
|
83
|
+
| {
|
|
84
|
+
kind: "partial";
|
|
85
|
+
data: T;
|
|
86
|
+
file: string;
|
|
87
|
+
reason: string;
|
|
88
|
+
/** Syntax error and missing-node ranges in 0-based UTF-16 coordinates. */
|
|
89
|
+
errorRanges: SourceRange[];
|
|
90
|
+
}
|
|
83
91
|
| { kind: "unsupported-language"; file: string; message: string }
|
|
84
92
|
| { kind: "file-access-error"; file: string; message: string }
|
|
85
93
|
| { kind: "validation-error"; message: string }
|
|
@@ -93,7 +101,10 @@ export type ConfidenceMode = "semantic" | "structural" | "heuristic" | "unavaila
|
|
|
93
101
|
|
|
94
102
|
export interface OutlineData {
|
|
95
103
|
name: string;
|
|
104
|
+
/** Common structural search category. */
|
|
96
105
|
kind: string;
|
|
106
|
+
/** Source-language declaration kind, separate from common search category. */
|
|
107
|
+
nativeKind?: string;
|
|
97
108
|
/** Exact 1-based start of the declaration name token, when known. */
|
|
98
109
|
nameAnchor?: SymbolAnchor;
|
|
99
110
|
/** Exclusive 1-based end of a verified literal name span, when known. */
|
|
@@ -156,6 +167,12 @@ export interface FileEdit {
|
|
|
156
167
|
/** A document-state precondition established when semantic edits are normalized. */
|
|
157
168
|
export type DocumentEditPrecondition =
|
|
158
169
|
| { file: string; kind: "open-document-version"; version: number }
|
|
170
|
+
| {
|
|
171
|
+
file: string;
|
|
172
|
+
kind: "open-document-content";
|
|
173
|
+
version: number;
|
|
174
|
+
fingerprint: string;
|
|
175
|
+
}
|
|
159
176
|
| { file: string; kind: "disk-content" };
|
|
160
177
|
|
|
161
178
|
/**
|
|
@@ -257,5 +274,10 @@ export interface CalleesData {
|
|
|
257
274
|
/** A single call-site match with name and start line. */
|
|
258
275
|
export interface CallSite {
|
|
259
276
|
name: string;
|
|
277
|
+
/** 1-based UTF-16 start position of the written callee expression. */
|
|
260
278
|
startLine: number;
|
|
279
|
+
startCharacter: number;
|
|
280
|
+
/** Exclusive 1-based UTF-16 end position of the captured source expression. */
|
|
281
|
+
endLine: number;
|
|
282
|
+
endCharacter: number;
|
|
261
283
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-tree-sitter",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"description": "Structural AST analysis for SuPi code intelligence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"jiti": "^2.7.0",
|
|
38
38
|
"web-tree-sitter": "^0.27.0",
|
|
39
|
-
"@mrclrchtr/supi-code-runtime": "
|
|
40
|
-
"@mrclrchtr/supi-core": "
|
|
39
|
+
"@mrclrchtr/supi-code-runtime": "11.0.0",
|
|
40
|
+
"@mrclrchtr/supi-core": "11.0.0"
|
|
41
41
|
},
|
|
42
42
|
"bundledDependencies": [
|
|
43
43
|
"@mrclrchtr/supi-code-runtime",
|
package/src/language.ts
CHANGED
|
@@ -7,9 +7,18 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import {
|
|
11
|
+
C_FAMILY_EXTENSIONS,
|
|
12
|
+
type CFamilyExtension,
|
|
13
|
+
type CFamilyFileAssociationResolver,
|
|
14
|
+
defaultCFamilyLanguage,
|
|
15
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
10
16
|
import type { GrammarId, SupportedExtension } from "./types.ts";
|
|
11
17
|
|
|
12
18
|
const sourceDir = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const C_FAMILY_GRAMMAR = Object.fromEntries(
|
|
20
|
+
C_FAMILY_EXTENSIONS.map((extension) => [extension, defaultCFamilyLanguage(extension)]),
|
|
21
|
+
) as Record<CFamilyExtension, GrammarId>;
|
|
13
22
|
|
|
14
23
|
/** Mapping from file extension to grammar identifier. */
|
|
15
24
|
const EXTENSION_GRAMMAR = {
|
|
@@ -25,15 +34,7 @@ const EXTENSION_GRAMMAR = {
|
|
|
25
34
|
".pyi": "python",
|
|
26
35
|
".rs": "rust",
|
|
27
36
|
".go": "go",
|
|
28
|
-
|
|
29
|
-
".h": "c",
|
|
30
|
-
".cpp": "cpp",
|
|
31
|
-
".hpp": "cpp",
|
|
32
|
-
".cc": "cpp",
|
|
33
|
-
".cxx": "cpp",
|
|
34
|
-
".hxx": "cpp",
|
|
35
|
-
".c++": "cpp",
|
|
36
|
-
".h++": "cpp",
|
|
37
|
+
...C_FAMILY_GRAMMAR,
|
|
37
38
|
".java": "java",
|
|
38
39
|
".kt": "kotlin",
|
|
39
40
|
".kts": "kotlin",
|
|
@@ -68,7 +69,12 @@ export function getSupportedExtensionGrammarEntries(): Array<
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/** Detect the grammar for a file. Returns undefined if unsupported. */
|
|
71
|
-
export function detectGrammar(
|
|
72
|
+
export function detectGrammar(
|
|
73
|
+
filePath: string,
|
|
74
|
+
associations?: CFamilyFileAssociationResolver,
|
|
75
|
+
): GrammarId | undefined {
|
|
76
|
+
const familyLanguage = associations?.languageForFile(filePath);
|
|
77
|
+
if (familyLanguage) return familyLanguage;
|
|
72
78
|
const ext = path.extname(filePath).toLowerCase();
|
|
73
79
|
return EXTENSION_GRAMMAR[ext as SupportedExtension];
|
|
74
80
|
}
|
package/src/operation-support.ts
CHANGED
|
@@ -4,6 +4,18 @@ import type { GrammarId, SupportedExtension } from "./types.ts";
|
|
|
4
4
|
|
|
5
5
|
/** Structural collection operations whose language support defines an AST Scan universe. */
|
|
6
6
|
const STRUCTURAL_SEARCH_OPERATIONS = ["outline", "imports", "exports", "call-sites"] as const;
|
|
7
|
+
const IMPORT_GRAMMARS: ReadonlySet<GrammarId> = new Set([
|
|
8
|
+
"javascript",
|
|
9
|
+
"typescript",
|
|
10
|
+
"tsx",
|
|
11
|
+
"python",
|
|
12
|
+
"java",
|
|
13
|
+
"go",
|
|
14
|
+
"rust",
|
|
15
|
+
"c",
|
|
16
|
+
"cpp",
|
|
17
|
+
"latex",
|
|
18
|
+
]);
|
|
7
19
|
|
|
8
20
|
/** One Tree-sitter structural collection operation used by code-aware search. */
|
|
9
21
|
export type StructuralSearchOperation = (typeof STRUCTURAL_SEARCH_OPERATIONS)[number];
|
|
@@ -33,12 +45,7 @@ export function supportsGrammarOperation(
|
|
|
33
45
|
case "outline":
|
|
34
46
|
return true;
|
|
35
47
|
case "imports":
|
|
36
|
-
return (
|
|
37
|
-
grammar === "javascript" ||
|
|
38
|
-
grammar === "typescript" ||
|
|
39
|
-
grammar === "tsx" ||
|
|
40
|
-
grammar === "latex"
|
|
41
|
-
);
|
|
48
|
+
return IMPORT_GRAMMARS.has(grammar);
|
|
42
49
|
case "exports":
|
|
43
50
|
return grammar === "javascript" || grammar === "typescript" || grammar === "tsx";
|
|
44
51
|
default:
|
|
@@ -70,6 +70,17 @@ function mapTreeSitterResult<T, U>(
|
|
|
70
70
|
switch (result.kind) {
|
|
71
71
|
case "success":
|
|
72
72
|
return { kind: "success", data: mapData(result.data) };
|
|
73
|
+
case "partial":
|
|
74
|
+
return {
|
|
75
|
+
kind: "partial",
|
|
76
|
+
data: mapData(result.data),
|
|
77
|
+
file: result.file,
|
|
78
|
+
reason: result.reason,
|
|
79
|
+
errorRanges: result.errorRanges.map((range) => ({
|
|
80
|
+
start: { line: range.startLine - 1, character: range.startCharacter - 1 },
|
|
81
|
+
end: { line: range.endLine - 1, character: range.endCharacter - 1 },
|
|
82
|
+
})),
|
|
83
|
+
};
|
|
73
84
|
case "unsupported-language":
|
|
74
85
|
return { kind: "unsupported-language", file: result.file, message: result.message };
|
|
75
86
|
case "file-access-error":
|
|
@@ -103,6 +114,7 @@ function mapOutlineItems(items: OutlineItem[]): OutlineData[] {
|
|
|
103
114
|
return items.map((item) => ({
|
|
104
115
|
name: item.name,
|
|
105
116
|
kind: item.kind,
|
|
117
|
+
...(item.nativeKind === undefined ? {} : { nativeKind: item.nativeKind }),
|
|
106
118
|
...(item.nameAnchor ? { nameAnchor: { ...item.nameAnchor } } : {}),
|
|
107
119
|
...(item.nameEndAnchor ? { nameEndAnchor: { ...item.nameEndAnchor } } : {}),
|
|
108
120
|
...takeRange(item.range),
|
|
@@ -156,5 +168,11 @@ function mapCalleesAtResult(result: CalleesAtResult): CalleesData {
|
|
|
156
168
|
}
|
|
157
169
|
|
|
158
170
|
function mapCallSites(matches: CallSiteMatch[]): CallSite[] {
|
|
159
|
-
return matches.map((
|
|
171
|
+
return matches.map((match) => ({
|
|
172
|
+
name: match.name,
|
|
173
|
+
startLine: match.startLine,
|
|
174
|
+
startCharacter: match.startCharacter,
|
|
175
|
+
endLine: match.endLine,
|
|
176
|
+
endCharacter: match.endCharacter,
|
|
177
|
+
}));
|
|
160
178
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Tree-sitter lifecycle controller — owns one Structural Worker for one cwd.
|
|
2
2
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
CFamilyFileAssociationResolver,
|
|
6
|
+
WorkspaceRuntime,
|
|
7
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
5
8
|
import { createTreeSitterProvider } from "../provider/tree-sitter-provider.ts";
|
|
6
9
|
import type { TreeSitterService, TreeSitterSession } from "../types.ts";
|
|
7
10
|
import { createTreeSitterSession } from "./session.ts";
|
|
@@ -32,13 +35,20 @@ export class TreeSitterRuntimeController {
|
|
|
32
35
|
readonly #cwd: string;
|
|
33
36
|
#state: TsControllerState = { kind: "initial" };
|
|
34
37
|
readonly #runtime: WorkspaceRuntime;
|
|
38
|
+
readonly #fileAssociations?: CFamilyFileAssociationResolver;
|
|
35
39
|
#pendingSession: TreeSitterSession | null = null;
|
|
36
40
|
#generation = 0;
|
|
37
41
|
#transition = Promise.resolve();
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
/** Create a controller with an optional workspace association snapshot. */
|
|
44
|
+
constructor(
|
|
45
|
+
cwd: string,
|
|
46
|
+
runtime: WorkspaceRuntime,
|
|
47
|
+
fileAssociations?: CFamilyFileAssociationResolver,
|
|
48
|
+
) {
|
|
40
49
|
this.#cwd = cwd;
|
|
41
50
|
this.#runtime = runtime;
|
|
51
|
+
this.#fileAssociations = fileAssociations;
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
get cwd(): string {
|
|
@@ -79,11 +89,15 @@ export class TreeSitterRuntimeController {
|
|
|
79
89
|
if (generation !== this.#generation) return supersededStart();
|
|
80
90
|
this.#state = { kind: "unavailable", reason: "Initializing Tree-sitter" };
|
|
81
91
|
|
|
82
|
-
const session = createTreeSitterSession(this.#cwd
|
|
92
|
+
const session = createTreeSitterSession(this.#cwd, {
|
|
93
|
+
fileAssociations: this.#fileAssociations?.rules,
|
|
94
|
+
});
|
|
83
95
|
this.#pendingSession = session;
|
|
84
96
|
try {
|
|
85
97
|
const probe = await session.canParse(fileURLToPath(import.meta.url));
|
|
86
|
-
if (probe.kind !== "success")
|
|
98
|
+
if (probe.kind !== "success") {
|
|
99
|
+
throw new Error(probe.kind === "partial" ? probe.reason : probe.message);
|
|
100
|
+
}
|
|
87
101
|
if (generation !== this.#generation || this.#pendingSession !== session) {
|
|
88
102
|
return supersededStart();
|
|
89
103
|
}
|
package/src/session/session.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Session factory — creates asynchronous Structural Worker proxies and owned sessions.
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CFamilyFileAssociationRule,
|
|
5
|
+
CodeRequestControl,
|
|
6
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
4
7
|
import type {
|
|
5
8
|
CalleesAtResult,
|
|
6
9
|
CallSiteMatch,
|
|
@@ -62,13 +65,17 @@ export function createTreeSitterService(client: StructuralWorkerClient): TreeSit
|
|
|
62
65
|
|
|
63
66
|
/**
|
|
64
67
|
* Create one Tree-sitter session bound to a working directory.
|
|
68
|
+
* Pass trusted workspace associations when the caller owns config loading.
|
|
65
69
|
* Await disposal to prove that its Structural Worker has terminated.
|
|
66
70
|
*/
|
|
67
71
|
export function createTreeSitterSession(
|
|
68
72
|
cwd: string,
|
|
69
|
-
options: {
|
|
73
|
+
options: {
|
|
74
|
+
readonly workerFactory?: StructuralWorkerFactory;
|
|
75
|
+
readonly fileAssociations?: readonly CFamilyFileAssociationRule[];
|
|
76
|
+
} = {},
|
|
70
77
|
): TreeSitterSession {
|
|
71
|
-
const client = new StructuralWorkerClient(cwd, options.workerFactory);
|
|
78
|
+
const client = new StructuralWorkerClient(cwd, options.workerFactory, options.fileAssociations);
|
|
72
79
|
const service = createTreeSitterService(client);
|
|
73
80
|
return {
|
|
74
81
|
...service,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// biome-ignore-all lint/style/noExcessiveLinesPerFile: one mailbox state machine owns all Worker lifecycle transitions
|
|
2
2
|
import { setImmediate as yieldImmediate } from "node:timers/promises";
|
|
3
3
|
import {
|
|
4
|
+
type CFamilyFileAssociationRule,
|
|
4
5
|
type CodeRequestControl,
|
|
5
6
|
CodeRequestDeadlineError,
|
|
6
7
|
throwIfCodeRequestInterrupted,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
export type StructuralWorkerFactory = (options: {
|
|
29
30
|
readonly cwd: string;
|
|
30
31
|
readonly generation: number;
|
|
32
|
+
readonly fileAssociations: readonly CFamilyFileAssociationRule[];
|
|
31
33
|
}) => StructuralWorkerLike;
|
|
32
34
|
|
|
33
35
|
const TEST_WORKER_FACTORY = Symbol.for("supi-tree-sitter/test-worker-factory");
|
|
@@ -74,6 +76,7 @@ interface ActiveWorker {
|
|
|
74
76
|
export class StructuralWorkerClient {
|
|
75
77
|
readonly #cwd: string;
|
|
76
78
|
readonly #factory: StructuralWorkerFactory;
|
|
79
|
+
readonly #fileAssociations: readonly CFamilyFileAssociationRule[];
|
|
77
80
|
#worker: ActiveWorker | null = null;
|
|
78
81
|
#queue: PendingRequest<unknown>[] = [];
|
|
79
82
|
#active: PendingRequest<unknown> | null = null;
|
|
@@ -86,9 +89,16 @@ export class StructuralWorkerClient {
|
|
|
86
89
|
readonly #lifecycle = new StructuralWorkerLifecycle();
|
|
87
90
|
#disposePromise: Promise<void> | null = null;
|
|
88
91
|
|
|
89
|
-
constructor(
|
|
92
|
+
constructor(
|
|
93
|
+
cwd: string,
|
|
94
|
+
factory?: StructuralWorkerFactory,
|
|
95
|
+
fileAssociations: readonly CFamilyFileAssociationRule[] = [],
|
|
96
|
+
) {
|
|
90
97
|
this.#cwd = cwd;
|
|
91
98
|
this.#factory = factory ?? testWorkerFactory() ?? createProductionWorker;
|
|
99
|
+
this.#fileAssociations = Object.freeze(
|
|
100
|
+
fileAssociations.map((rule) => Object.freeze({ ...rule })),
|
|
101
|
+
);
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
/** Start and validate the owned Worker without running parser work on the parent. */
|
|
@@ -187,7 +197,11 @@ export class StructuralWorkerClient {
|
|
|
187
197
|
|
|
188
198
|
async #startGeneration(): Promise<void> {
|
|
189
199
|
const generation = ++this.#generation;
|
|
190
|
-
const worker = this.#factory({
|
|
200
|
+
const worker = this.#factory({
|
|
201
|
+
cwd: this.#cwd,
|
|
202
|
+
generation,
|
|
203
|
+
fileAssociations: this.#fileAssociations,
|
|
204
|
+
});
|
|
191
205
|
let rejectStartup = (_message: string) => {};
|
|
192
206
|
const active: ActiveWorker = {
|
|
193
207
|
generation,
|
|
@@ -563,6 +577,7 @@ function testWorkerFactory(): StructuralWorkerFactory | undefined {
|
|
|
563
577
|
function createProductionWorker(options: {
|
|
564
578
|
cwd: string;
|
|
565
579
|
generation: number;
|
|
580
|
+
fileAssociations: readonly CFamilyFileAssociationRule[];
|
|
566
581
|
}): StructuralWorkerLike {
|
|
567
582
|
return createStructuralWorkerProcess(options);
|
|
568
583
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { type ChildProcess, fork } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { CFamilyFileAssociationRule } from "@mrclrchtr/supi-code-runtime/api";
|
|
4
5
|
import { decodeStructuralWorkerJsonMessage } from "./structural-worker-json.ts";
|
|
5
6
|
import { resolveStructuralWorkerNode } from "./structural-worker-node.ts";
|
|
6
|
-
import
|
|
7
|
-
ParentToStructuralWorkerMessage,
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
type ParentToStructuralWorkerMessage,
|
|
9
|
+
STRUCTURAL_WORKER_PROTOCOL_VERSION,
|
|
10
|
+
type StructuralWorkerInitializationMessage,
|
|
11
|
+
type StructuralWorkerToParentMessage,
|
|
9
12
|
} from "./structural-worker-protocol.ts";
|
|
10
13
|
|
|
11
14
|
const PARENT_CHANNEL = "supi-tree-sitter-parent";
|
|
@@ -23,6 +26,7 @@ export interface StructuralWorkerLike {
|
|
|
23
26
|
export function createStructuralWorkerProcess(options: {
|
|
24
27
|
readonly cwd: string;
|
|
25
28
|
readonly generation: number;
|
|
29
|
+
readonly fileAssociations: readonly CFamilyFileAssociationRule[];
|
|
26
30
|
}): StructuralWorkerLike {
|
|
27
31
|
const hostPath = fileURLToPath(new URL("../worker/process-host.mjs", import.meta.url));
|
|
28
32
|
const jsonIpc = Boolean(process.versions.bun);
|
|
@@ -36,7 +40,13 @@ export function createStructuralWorkerProcess(options: {
|
|
|
36
40
|
stdio: ["ignore", "ignore", "inherit", "ipc"],
|
|
37
41
|
windowsHide: true,
|
|
38
42
|
});
|
|
39
|
-
return new StructuralWorkerProcess(child, jsonIpc
|
|
43
|
+
return new StructuralWorkerProcess(child, jsonIpc, {
|
|
44
|
+
kind: "initialize",
|
|
45
|
+
version: STRUCTURAL_WORKER_PROTOCOL_VERSION,
|
|
46
|
+
generation: options.generation,
|
|
47
|
+
cwd: options.cwd,
|
|
48
|
+
fileAssociations: options.fileAssociations,
|
|
49
|
+
});
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
class StructuralWorkerProcess implements StructuralWorkerLike {
|
|
@@ -44,12 +54,19 @@ class StructuralWorkerProcess implements StructuralWorkerLike {
|
|
|
44
54
|
readonly #child: ChildProcess;
|
|
45
55
|
readonly #jsonIpc: boolean;
|
|
46
56
|
|
|
47
|
-
constructor(
|
|
57
|
+
constructor(
|
|
58
|
+
child: ChildProcess,
|
|
59
|
+
jsonIpc: boolean,
|
|
60
|
+
initialization: StructuralWorkerInitializationMessage,
|
|
61
|
+
) {
|
|
48
62
|
this.#child = child;
|
|
49
63
|
this.#jsonIpc = jsonIpc;
|
|
50
64
|
child.on("message", (value: unknown) => this.#receive(value));
|
|
51
65
|
child.on("error", (error) => this.#events.emit("error", error));
|
|
52
66
|
child.on("exit", (code) => this.#events.emit("exit", code ?? 1));
|
|
67
|
+
child.send({ channel: PARENT_CHANNEL, message: initialization }, (error) => {
|
|
68
|
+
if (error) this.#events.emit("error", error);
|
|
69
|
+
});
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
postMessage(message: ParentToStructuralWorkerMessage): void {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// biome-ignore-all lint/style/noExcessiveLinesPerFile: one private protocol module defines and validates the full closed contract
|
|
2
|
+
|
|
3
|
+
import type { CFamilyFileAssociationRule } from "@mrclrchtr/supi-code-runtime/api";
|
|
2
4
|
import { isDebugOperationId } from "@mrclrchtr/supi-core/debug";
|
|
3
5
|
import type { TreeSitterResult } from "../types.ts";
|
|
4
6
|
import type { StructuralTimingEvent } from "./structural-timing.ts";
|
|
@@ -37,6 +39,14 @@ export type StructuralWorkerOperation =
|
|
|
37
39
|
readonly depth?: "direct" | "deep";
|
|
38
40
|
};
|
|
39
41
|
|
|
42
|
+
export interface StructuralWorkerInitializationMessage {
|
|
43
|
+
readonly kind: "initialize";
|
|
44
|
+
readonly version: typeof STRUCTURAL_WORKER_PROTOCOL_VERSION;
|
|
45
|
+
readonly generation: number;
|
|
46
|
+
readonly cwd: string;
|
|
47
|
+
readonly fileAssociations: readonly CFamilyFileAssociationRule[];
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
export interface StructuralWorkerRequestMessage {
|
|
41
51
|
readonly kind: "request";
|
|
42
52
|
readonly version: typeof STRUCTURAL_WORKER_PROTOCOL_VERSION;
|
|
@@ -427,7 +437,18 @@ function isStructuralTimingEvent(value: unknown): value is StructuralTimingEvent
|
|
|
427
437
|
function assertTreeSitterResult(value: unknown): asserts value is TreeSitterResult<unknown> {
|
|
428
438
|
if (!isRecord(value) || typeof value.kind !== "string")
|
|
429
439
|
throw new Error("Invalid structural result");
|
|
430
|
-
if (value.kind === "success" && "data"
|
|
440
|
+
if (value.kind === "success" && hasExactKeys(value, ["kind", "data"])) return;
|
|
441
|
+
if (
|
|
442
|
+
value.kind === "partial" &&
|
|
443
|
+
hasExactKeys(value, ["kind", "data", "file", "reason", "errorRanges"]) &&
|
|
444
|
+
isFileValue(value.file) &&
|
|
445
|
+
isBoundedString(value.reason, 2_048) &&
|
|
446
|
+
Array.isArray(value.errorRanges) &&
|
|
447
|
+
value.errorRanges.length <= 10_000 &&
|
|
448
|
+
value.errorRanges.every(isTreeSitterSourceRange)
|
|
449
|
+
) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
431
452
|
if (
|
|
432
453
|
value.kind === "unsupported-language" ||
|
|
433
454
|
value.kind === "file-access-error" ||
|
|
@@ -527,6 +548,20 @@ function estimateChunkMessageBytes(payloadBytes: number): number {
|
|
|
527
548
|
return payloadBytes + 1024;
|
|
528
549
|
}
|
|
529
550
|
|
|
551
|
+
function isTreeSitterSourceRange(value: unknown): boolean {
|
|
552
|
+
return (
|
|
553
|
+
isRecord(value) &&
|
|
554
|
+
hasExactKeys(value, ["startLine", "startCharacter", "endLine", "endCharacter"]) &&
|
|
555
|
+
isPositiveInteger(value.startLine) &&
|
|
556
|
+
isPositiveInteger(value.startCharacter) &&
|
|
557
|
+
isPositiveInteger(value.endLine) &&
|
|
558
|
+
isPositiveInteger(value.endCharacter) &&
|
|
559
|
+
(Number(value.startLine) < Number(value.endLine) ||
|
|
560
|
+
(value.startLine === value.endLine &&
|
|
561
|
+
Number(value.startCharacter) <= Number(value.endCharacter)))
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
530
565
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
531
566
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
532
567
|
}
|