surgent 0.7.0-alpha.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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +407 -0
  3. package/bin/surgent.js +211 -0
  4. package/dist/optimizers/LICENSE +21 -0
  5. package/dist/optimizers/index.js +1984 -0
  6. package/dist/optimizers/index.js.map +7 -0
  7. package/dist/optimizers/package.json +31 -0
  8. package/package.json +45 -0
  9. package/src/agent/built-in/documenter.md +58 -0
  10. package/src/agent/built-in/general.md +107 -0
  11. package/src/agent/built-in/planner.md +73 -0
  12. package/src/agent/built-in/scout.md +97 -0
  13. package/src/agent/command.ts +140 -0
  14. package/src/agent/helpers.ts +95 -0
  15. package/src/agent/index.ts +9 -0
  16. package/src/agent/storage.ts +287 -0
  17. package/src/agent/types.ts +28 -0
  18. package/src/checkpoint/git.ts +173 -0
  19. package/src/checkpoint/index.ts +117 -0
  20. package/src/checkpoint/snapshot.ts +28 -0
  21. package/src/checkpoint/stage.ts +59 -0
  22. package/src/checkpoint/store.ts +108 -0
  23. package/src/cleanup/checkpoint.ts +31 -0
  24. package/src/cleanup/helpers.ts +24 -0
  25. package/src/cleanup/index.ts +21 -0
  26. package/src/cleanup/permission.ts +74 -0
  27. package/src/cleanup/subsession.ts +46 -0
  28. package/src/commands/helpers.ts +217 -0
  29. package/src/commands/index.ts +79 -0
  30. package/src/commands/render.ts +95 -0
  31. package/src/commands/types.ts +11 -0
  32. package/src/mcp-client/call-tool.ts +143 -0
  33. package/src/mcp-client/client.ts +90 -0
  34. package/src/mcp-client/command.ts +257 -0
  35. package/src/mcp-client/helpers.ts +153 -0
  36. package/src/mcp-client/index.ts +21 -0
  37. package/src/mcp-client/list-tools.ts +84 -0
  38. package/src/mcp-client/storage.ts +190 -0
  39. package/src/mcp-client/types.ts +34 -0
  40. package/src/mcp-client/validation.ts +115 -0
  41. package/src/optimizers/compactor/bash.ts +159 -0
  42. package/src/optimizers/compactor/grep.ts +141 -0
  43. package/src/optimizers/compactor/index.ts +132 -0
  44. package/src/optimizers/deduplicator/helpers.ts +75 -0
  45. package/src/optimizers/deduplicator/index.ts +23 -0
  46. package/src/optimizers/deduplicator/resources.ts +77 -0
  47. package/src/optimizers/deduplicator/state.ts +119 -0
  48. package/src/optimizers/deduplicator/types.ts +14 -0
  49. package/src/optimizers/entries.ts +104 -0
  50. package/src/optimizers/index.ts +17 -0
  51. package/src/optimizers/inspector/helpers.ts +60 -0
  52. package/src/optimizers/inspector/index.ts +89 -0
  53. package/src/optimizers/inspector/inspect.ts +88 -0
  54. package/src/optimizers/inspector/types.ts +7 -0
  55. package/src/optimizers/languages/go.ts +79 -0
  56. package/src/optimizers/languages/grammar.ts +200 -0
  57. package/src/optimizers/languages/index.ts +75 -0
  58. package/src/optimizers/languages/java.ts +64 -0
  59. package/src/optimizers/languages/python.ts +63 -0
  60. package/src/optimizers/languages/rust.ts +71 -0
  61. package/src/optimizers/languages/symbols.ts +95 -0
  62. package/src/optimizers/languages/tree-sitter-languages.d.ts +23 -0
  63. package/src/optimizers/languages/types.ts +134 -0
  64. package/src/optimizers/languages/typescript.ts +116 -0
  65. package/src/optimizers/mapper/files.ts +94 -0
  66. package/src/optimizers/mapper/index.ts +133 -0
  67. package/src/optimizers/mapper/types.ts +6 -0
  68. package/src/optimizers/pruner/cleanup.ts +121 -0
  69. package/src/optimizers/pruner/context.ts +46 -0
  70. package/src/optimizers/pruner/index.ts +45 -0
  71. package/src/optimizers/pruner/session.ts +34 -0
  72. package/src/optimizers/pruner/types.ts +18 -0
  73. package/src/permission/bash.ts +124 -0
  74. package/src/permission/command.ts +111 -0
  75. package/src/permission/components/prompt.ts +255 -0
  76. package/src/permission/components/rules-list.ts +342 -0
  77. package/src/permission/constants.ts +48 -0
  78. package/src/permission/helpers.ts +156 -0
  79. package/src/permission/index.ts +134 -0
  80. package/src/permission/pattern.ts +51 -0
  81. package/src/permission/piignore.ts +148 -0
  82. package/src/permission/precedence.ts +54 -0
  83. package/src/permission/resolution.ts +116 -0
  84. package/src/permission/storage.ts +142 -0
  85. package/src/permission/types.ts +57 -0
  86. package/src/questionnaire/component.ts +357 -0
  87. package/src/questionnaire/helpers.ts +220 -0
  88. package/src/questionnaire/index.ts +67 -0
  89. package/src/questionnaire/schemas.ts +50 -0
  90. package/src/questionnaire/types.ts +47 -0
  91. package/src/redactor/index.ts +34 -0
  92. package/src/redactor/patterns.ts +234 -0
  93. package/src/redactor/secrets.ts +113 -0
  94. package/src/subagent/helpers.ts +93 -0
  95. package/src/subagent/index.ts +81 -0
  96. package/src/subagent/storage.ts +100 -0
  97. package/src/subagent/subsession.ts +266 -0
  98. package/src/subagent/types.ts +83 -0
  99. package/src/subagent/validation.ts +100 -0
  100. package/src/ui/components/action-select-list.ts +165 -0
  101. package/src/ui/components/bash-mode.ts +281 -0
  102. package/src/ui/components/extended-select-list.ts +166 -0
  103. package/src/ui/components/form-field.ts +184 -0
  104. package/src/ui/components/form.ts +179 -0
  105. package/src/ui/components/frame.ts +60 -0
  106. package/src/ui/components/input-mode-indicator.ts +64 -0
  107. package/src/ui/components/keybound.ts +150 -0
  108. package/src/ui/components/lines.ts +27 -0
  109. package/src/ui/components/placeholder-input.ts +59 -0
  110. package/src/ui/components/scoped-input.ts +78 -0
  111. package/src/ui/components/scrollable-view.ts +155 -0
  112. package/src/ui/index.ts +40 -0
  113. package/src/utils.ts +206 -0
  114. package/src/web-tools/index.ts +15 -0
  115. package/src/web-tools/providers/brave.ts +55 -0
  116. package/src/web-tools/providers/firecrawl.ts +66 -0
  117. package/src/web-tools/providers/index.ts +50 -0
  118. package/src/web-tools/providers/jina.ts +48 -0
  119. package/src/web-tools/providers/native.ts +57 -0
  120. package/src/web-tools/providers/tavily.ts +56 -0
  121. package/src/web-tools/settings.ts +15 -0
  122. package/src/web-tools/web-fetch/helpers.ts +66 -0
  123. package/src/web-tools/web-fetch/index.ts +91 -0
  124. package/src/web-tools/web-fetch/parser.ts +51 -0
  125. package/src/web-tools/web-fetch/storage.ts +65 -0
  126. package/src/web-tools/web-fetch/types.ts +8 -0
  127. package/src/web-tools/web-login/helpers.ts +79 -0
  128. package/src/web-tools/web-login/index.ts +100 -0
  129. package/src/web-tools/web-login/types.ts +4 -0
  130. package/src/web-tools/web-search/helpers.ts +36 -0
  131. package/src/web-tools/web-search/index.ts +98 -0
  132. package/src/web-tools/web-search/types.ts +15 -0
@@ -0,0 +1,200 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { LANGUAGE_REGISTRY } from "./index.js";
6
+ import type { GrammarInstallSettings } from "./types.js";
7
+ import { getPiPath, isMissingFileError, readJson, runCommand, writeJson } from "../../utils.js";
8
+ import { createRequire } from "node:module";
9
+ import { pathToFileURL } from "node:url";
10
+
11
+ const GRAMMAR_COVERAGE_TARGET = 0.9;
12
+ const GRAMMAR_INSTALL_KEY = "grammarInstall";
13
+
14
+ async function ensureGrammarCachePackage() {
15
+ const grammarCacheDir = getPiPath("grammars");
16
+ const jsonPath = resolve(grammarCacheDir, "package.json");
17
+
18
+ try {
19
+ await readFile(jsonPath, "utf8");
20
+ } catch (error) {
21
+ if (!isMissingFileError(error)) {
22
+ throw error;
23
+ }
24
+ await writeJson(jsonPath, {
25
+ name: "tree-sitter-grammars",
26
+ private: true,
27
+ dependencies: {},
28
+ });
29
+ }
30
+ }
31
+
32
+ async function getRepositoryRoot(cwd: string) {
33
+ try {
34
+ const { stdout } = await runCommand(cwd, "git", ["rev-parse", "--show-toplevel"]);
35
+ const repoRoot = stdout.trim();
36
+ return repoRoot.length > 0 ? repoRoot : undefined;
37
+ } catch {}
38
+ }
39
+
40
+ async function selectSupportedBuckets(cwd: string) {
41
+ const bucketCounts = new Map<string, number>();
42
+ let totalFileCount = 0;
43
+
44
+ const { stdout } = await runCommand(cwd, "bash", [
45
+ "-lc",
46
+ "git ls-files | sed 's/.*\\.//' | sort | uniq -c | sort -rn",
47
+ ]);
48
+
49
+ for (const line of stdout.split(/\r?\n/)) {
50
+ const lineMatch = line.trim().match(/^(\d+)\s+(.+)$/);
51
+ if (!lineMatch) continue;
52
+
53
+ const rawCount = lineMatch[1];
54
+ const rawExtension = lineMatch[2];
55
+ if (!rawCount || !rawExtension) continue;
56
+
57
+ const count = Number.parseInt(rawCount, 10);
58
+ const extension = `.${rawExtension.toLowerCase()}`;
59
+ if (!Number.isFinite(count) || count <= 0) continue;
60
+
61
+ totalFileCount += count;
62
+ for (const languageEntry of LANGUAGE_REGISTRY) {
63
+ if (!languageEntry.profile.extensions.has(extension)) continue;
64
+ const nextCount = bucketCounts.get(languageEntry.bucketName) ?? 0;
65
+ bucketCounts.set(languageEntry.bucketName, nextCount + count);
66
+ break;
67
+ }
68
+ }
69
+
70
+ if (totalFileCount === 0) return [];
71
+ const rankedBuckets = LANGUAGE_REGISTRY.map((languageEntry) => ({
72
+ count: bucketCounts.get(languageEntry.bucketName) ?? 0,
73
+ packageName: languageEntry.packageName,
74
+ version: languageEntry.version,
75
+ }))
76
+ .filter((bucket) => bucket.count > 0)
77
+ .sort((leftBucket, rightBucket) => rightBucket.count - leftBucket.count);
78
+
79
+ let coveredCount = 0;
80
+ const pickedBuckets: Array<{ packageName: string; version: string }> = [];
81
+ for (const bucket of rankedBuckets) {
82
+ if (coveredCount / totalFileCount >= GRAMMAR_COVERAGE_TARGET) break;
83
+ coveredCount += bucket.count;
84
+ pickedBuckets.push({
85
+ packageName: bucket.packageName,
86
+ version: bucket.version,
87
+ });
88
+ }
89
+
90
+ return pickedBuckets;
91
+ }
92
+
93
+ function getMissingGrammarPackages(pickedBuckets: Array<{ packageName: string; version: string }>) {
94
+ const grammarCacheDir = getPiPath("grammars");
95
+
96
+ return pickedBuckets
97
+ .filter((bucket) => {
98
+ const packagePath = resolve(
99
+ grammarCacheDir,
100
+ "node_modules",
101
+ bucket.packageName,
102
+ "package.json",
103
+ );
104
+ return !existsSync(packagePath);
105
+ })
106
+ .map((bucket) => `${bucket.packageName}@${bucket.version}`);
107
+ }
108
+
109
+ function readGrammarInstallSettings(settings: Record<string, unknown>) {
110
+ const rawSettings = settings[GRAMMAR_INSTALL_KEY];
111
+ if (!rawSettings || typeof rawSettings !== "object") return {};
112
+
113
+ const settingsMap = rawSettings as Record<string, unknown>;
114
+ const allowed = Array.isArray(settingsMap.allowedRepos)
115
+ ? settingsMap.allowedRepos.filter((repo) => typeof repo === "string")
116
+ : undefined;
117
+ const denied = Array.isArray(settingsMap.deniedRepos)
118
+ ? settingsMap.deniedRepos.filter((repo) => typeof repo === "string")
119
+ : undefined;
120
+
121
+ return { allowedRepos: allowed, deniedRepos: denied } satisfies GrammarInstallSettings;
122
+ }
123
+
124
+ async function updateGrammarInstallSettings(
125
+ settings: Record<string, unknown>,
126
+ installSettings: GrammarInstallSettings,
127
+ ) {
128
+ settings[GRAMMAR_INSTALL_KEY] = installSettings;
129
+ await writeJson(getPiPath("settings"), settings);
130
+ }
131
+
132
+ export async function ensureGrammarCache(ctx: ExtensionContext) {
133
+ await ensureGrammarCachePackage();
134
+
135
+ const repoRoot = await getRepositoryRoot(ctx.cwd);
136
+ if (!repoRoot) return;
137
+
138
+ const settings = await readJson<Record<string, unknown>>(getPiPath("settings"), {});
139
+ const installSettings = readGrammarInstallSettings(settings);
140
+ const allowed = new Set(installSettings.allowedRepos ?? []);
141
+ const denied = new Set(installSettings.deniedRepos ?? []);
142
+ if (denied.has(repoRoot)) return;
143
+
144
+ const pickedBuckets = await selectSupportedBuckets(ctx.cwd);
145
+ if (pickedBuckets.length === 0) return;
146
+
147
+ const missingPkgs = getMissingGrammarPackages(pickedBuckets);
148
+ if (missingPkgs.length === 0) return;
149
+
150
+ let canInstall = allowed.has(repoRoot);
151
+ if (!canInstall) {
152
+ if (!ctx.hasUI) return;
153
+
154
+ canInstall = await ctx.ui.confirm(
155
+ "Install tree-sitter grammars for this repo?",
156
+ "Surgent installs missing parser grammars for optimized code read, input tokens and context optimizations.",
157
+ );
158
+
159
+ if (!canInstall) {
160
+ denied.add(repoRoot);
161
+ await updateGrammarInstallSettings(settings, {
162
+ allowedRepos: [...allowed],
163
+ deniedRepos: [...denied],
164
+ });
165
+ return;
166
+ }
167
+
168
+ allowed.add(repoRoot);
169
+ await updateGrammarInstallSettings(settings, {
170
+ allowedRepos: [...allowed],
171
+ deniedRepos: [...denied],
172
+ });
173
+ }
174
+
175
+ await runCommand(getPiPath("grammars"), "npm", ["install", "--save-exact", ...missingPkgs]);
176
+ }
177
+
178
+ export async function loadGrammarModule(pkgName: string) {
179
+ const grammarCacheDir = getPiPath("grammars");
180
+ const cacheRequire = createRequire(resolve(grammarCacheDir, "package.json"));
181
+
182
+ let entryPath = "";
183
+ try {
184
+ entryPath = cacheRequire.resolve(pkgName);
185
+ } catch (error) {
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ throw new Error(
188
+ `grammar package not installed in cache (${grammarCacheDir}): ${pkgName}. ${message}`,
189
+ );
190
+ }
191
+
192
+ try {
193
+ return await import(pathToFileURL(entryPath).href);
194
+ } catch (error) {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ throw new Error(
197
+ `failed loading grammar module from cache (${grammarCacheDir}): ${pkgName}. ${message}`,
198
+ );
199
+ }
200
+ }
@@ -0,0 +1,75 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { ensureGrammarCache } from "./grammar.js";
3
+ import type { LanguageProfile } from "./types.js";
4
+ import { TypeScriptLanguageProfile } from "./typescript.js";
5
+ import { PythonLanguageProfile } from "./python.js";
6
+ import { GoLanguageProfile } from "./go.js";
7
+ import { JavaLanguageProfile } from "./java.js";
8
+ import { RustLanguageProfile } from "./rust.js";
9
+
10
+ export const LANGUAGE_REGISTRY: Array<{
11
+ bucketName: string;
12
+ packageName: string;
13
+ version: string;
14
+ profile: LanguageProfile;
15
+ }> = [
16
+ {
17
+ bucketName: "typescript",
18
+ packageName: "tree-sitter-typescript",
19
+ version: "0.23.2",
20
+ profile: new TypeScriptLanguageProfile(),
21
+ },
22
+ {
23
+ bucketName: "python",
24
+ packageName: "tree-sitter-python",
25
+ version: "0.25.0",
26
+ profile: new PythonLanguageProfile(),
27
+ },
28
+ {
29
+ bucketName: "go",
30
+ packageName: "tree-sitter-go",
31
+ version: "0.25.0",
32
+ profile: new GoLanguageProfile(),
33
+ },
34
+ {
35
+ bucketName: "java",
36
+ packageName: "tree-sitter-java",
37
+ version: "0.23.5",
38
+ profile: new JavaLanguageProfile(),
39
+ },
40
+ {
41
+ bucketName: "rust",
42
+ packageName: "tree-sitter-rust",
43
+ version: "0.24.0",
44
+ profile: new RustLanguageProfile(),
45
+ },
46
+ ];
47
+
48
+ export function getLanguageProfile(extension: string) {
49
+ return LANGUAGE_REGISTRY.find(({ profile }) => profile.extensions.has(extension))?.profile;
50
+ }
51
+
52
+ export function getSupportedExtensions() {
53
+ const supported = new Set<string>();
54
+ LANGUAGE_REGISTRY.forEach(({ profile }) => {
55
+ profile.extensions.forEach((extension) => supported.add(extension));
56
+ });
57
+ return supported;
58
+ }
59
+
60
+ export default function (pi: ExtensionAPI) {
61
+ pi.on("session_start", async (_event, ctx) => {
62
+ try {
63
+ await ensureGrammarCache(ctx);
64
+ } catch (error) {
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ if (ctx.hasUI) {
67
+ ctx.ui.notify(`tree-sitter grammar install failed: ${message}`, "error");
68
+ }
69
+ }
70
+ });
71
+ }
72
+
73
+ export { collectSymbols } from "./symbols.js";
74
+ export type { LanguageProfile, LanguageSymbol } from "./types.js";
75
+ export const SYMBOL_KINDS = ["func", "class", "method", "decl", "deps", "public"] as const;
@@ -0,0 +1,64 @@
1
+ import type { Language, SyntaxNode } from "tree-sitter";
2
+ import { loadGrammarModule } from "./grammar.js";
3
+ import { RuleBasedLanguageProfile, type SymbolKindRule } from "./types.js";
4
+
5
+ export class JavaLanguageProfile extends RuleBasedLanguageProfile {
6
+ constructor() {
7
+ super(
8
+ new Set([".java"]),
9
+ { __default__: "name", exports_module_directive: "package" },
10
+ new Set(["program"]),
11
+ new Set<string>(),
12
+ new Map<string, SymbolKindRule[]>([
13
+ ["annotation_type_declaration", [{ kind: "class" }]],
14
+ ["class_declaration", [{ kind: "class" }]],
15
+ ["enum_declaration", [{ kind: "class" }]],
16
+ ["interface_declaration", [{ kind: "class" }]],
17
+ ["record_declaration", [{ kind: "class" }]],
18
+ [
19
+ "constructor_declaration",
20
+ [
21
+ { kind: "method", parent: "class_body" },
22
+ { kind: "method", parent: "enum_body_declarations" },
23
+ ],
24
+ ],
25
+ [
26
+ "method_declaration",
27
+ [
28
+ { kind: "method", parent: "class_body" },
29
+ { kind: "method", parent: "enum_body_declarations" },
30
+ { kind: "method", parent: "interface_body" },
31
+ ],
32
+ ],
33
+ ["import_declaration", [{ kind: "deps" }]],
34
+ ["exports_module_directive", [{ kind: "public", parent: "module_body" }]],
35
+ ]),
36
+ );
37
+ }
38
+
39
+ async loadLanguage(_extension: string) {
40
+ const languagePack = await loadGrammarModule("tree-sitter-java");
41
+ const languageExport = (languagePack.default ?? languagePack) as Language;
42
+ return languageExport;
43
+ }
44
+
45
+ readNodeName(node: SyntaxNode) {
46
+ const nodeName = this.readNameField(node);
47
+ if (nodeName) {
48
+ return nodeName;
49
+ }
50
+
51
+ if (node.type === "import_declaration") {
52
+ const importText = node.text
53
+ .replace(/^import\s+/, "")
54
+ .replace(/\s*;\s*$/, "")
55
+ .trim()
56
+ .replaceAll("\n", " ");
57
+ return importText.length > 0 ? importText : undefined;
58
+ }
59
+
60
+ if (node.type === "identifier" || node.type === "scoped_identifier") {
61
+ return this.readNodeText(node);
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,63 @@
1
+ import type { Language, SyntaxNode } from "tree-sitter";
2
+ import { loadGrammarModule } from "./grammar.js";
3
+ import { RuleBasedLanguageProfile, type SymbolKindRule } from "./types.js";
4
+
5
+ export class PythonLanguageProfile extends RuleBasedLanguageProfile {
6
+ constructor() {
7
+ super(
8
+ new Set([".py"]),
9
+ {
10
+ __default__: "name",
11
+ assignment: "left",
12
+ future_import_statement: "name",
13
+ import_from_statement: "name",
14
+ import_statement: "name",
15
+ },
16
+ new Set(["module"]),
17
+ new Set(["decorated_definition", "expression_statement"]),
18
+ new Map<string, SymbolKindRule[]>([
19
+ ["class_definition", [{ kind: "class" }]],
20
+ [
21
+ "function_definition",
22
+ [
23
+ { kind: "method", container: "class_definition" },
24
+ { kind: "func", topLevelOnly: true },
25
+ ],
26
+ ],
27
+ ["assignment", [{ kind: "decl", topLevelOnly: true }]],
28
+ ["future_import_statement", [{ kind: "deps", topLevelOnly: true }]],
29
+ ["import_from_statement", [{ kind: "deps", topLevelOnly: true }]],
30
+ ["import_statement", [{ kind: "deps", topLevelOnly: true }]],
31
+ ]),
32
+ );
33
+ }
34
+
35
+ async loadLanguage(_extension: string) {
36
+ const languagePack = await loadGrammarModule("tree-sitter-python");
37
+ const languageExport = (languagePack.default ?? languagePack) as Language;
38
+ return languageExport;
39
+ }
40
+
41
+ resolveSymbolKind(node: SyntaxNode) {
42
+ if (
43
+ node.type === "assignment" &&
44
+ this.matchesTopLevelRule(node) &&
45
+ this.readFieldText(node, "left") === "__all__"
46
+ ) {
47
+ return "public";
48
+ }
49
+
50
+ return super.resolveSymbolKind(node);
51
+ }
52
+
53
+ readNodeName(node: SyntaxNode) {
54
+ const nodeName = this.readNameField(node);
55
+ if (nodeName) {
56
+ return nodeName;
57
+ }
58
+
59
+ if (node.type === "dotted_name" || node.type === "identifier") {
60
+ return this.readNodeText(node);
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,71 @@
1
+ import type { Language, SyntaxNode } from "tree-sitter";
2
+ import { loadGrammarModule } from "./grammar.js";
3
+ import { RuleBasedLanguageProfile, type SymbolKindRule } from "./types.js";
4
+
5
+ export class RustLanguageProfile extends RuleBasedLanguageProfile {
6
+ constructor() {
7
+ super(
8
+ new Set([".rs"]),
9
+ { __default__: "name", use_declaration: "argument" },
10
+ new Set(["source_file"]),
11
+ new Set(["declaration_list", "foreign_mod_item", "mod_item"]),
12
+ new Map<string, SymbolKindRule[]>([
13
+ [
14
+ "function_item",
15
+ [
16
+ { kind: "method", container: "impl_item" },
17
+ { kind: "method", container: "trait_item" },
18
+ { kind: "func", topLevelOnly: true },
19
+ ],
20
+ ],
21
+ [
22
+ "function_signature_item",
23
+ [
24
+ { kind: "method", container: "trait_item" },
25
+ { kind: "func", topLevelOnly: true },
26
+ ],
27
+ ],
28
+ ["struct_item", [{ kind: "class", topLevelOnly: true }]],
29
+ ["enum_item", [{ kind: "class", topLevelOnly: true }]],
30
+ ["union_item", [{ kind: "class", topLevelOnly: true }]],
31
+ ["trait_item", [{ kind: "class", topLevelOnly: true }]],
32
+ ["type_item", [{ kind: "class", topLevelOnly: true }]],
33
+ ["mod_item", [{ kind: "class", topLevelOnly: true }]],
34
+ ["impl_item", [{ kind: "class" }]],
35
+ ["macro_definition", [{ kind: "func", topLevelOnly: true }]],
36
+ ["const_item", [{ kind: "decl", topLevelOnly: true }]],
37
+ ["static_item", [{ kind: "decl", topLevelOnly: true }]],
38
+ ["use_declaration", [{ kind: "deps", topLevelOnly: true }]],
39
+ ]),
40
+ );
41
+ }
42
+
43
+ async loadLanguage(_extension: string) {
44
+ const languagePack = await loadGrammarModule("tree-sitter-rust");
45
+ const languageExport = (languagePack.default ?? languagePack) as Language;
46
+ return languageExport;
47
+ }
48
+
49
+ readNodeName(node: SyntaxNode) {
50
+ if (node.type === "impl_item") {
51
+ return this.readFieldText(node, "type");
52
+ }
53
+
54
+ const nodeName = this.readNameField(node);
55
+ if (nodeName) {
56
+ return nodeName;
57
+ }
58
+
59
+ if (node.type === "identifier" || node.type === "type_identifier") {
60
+ return this.readNodeText(node);
61
+ }
62
+ }
63
+
64
+ isPublicSymbol(node: SyntaxNode) {
65
+ return node.namedChildren.some((childNode) => childNode.type === "visibility_modifier");
66
+ }
67
+
68
+ shouldSkipSymbol(node: SyntaxNode) {
69
+ return node.type === "impl_item";
70
+ }
71
+ }
@@ -0,0 +1,95 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import type { SyntaxNode } from "tree-sitter";
3
+ import { getLanguageProfile } from "./index.js";
4
+ import type { LanguageSymbol, SymbolKind } from "./types.js";
5
+ import { extname, resolve } from "node:path";
6
+ import Parser from "tree-sitter";
7
+
8
+ async function createCodeParser(extension: string) {
9
+ const languageProfile = getLanguageProfile(extension);
10
+ if (!languageProfile) {
11
+ throw new Error(`missing grammar for extension: ${extension}`);
12
+ }
13
+ const parser = new Parser();
14
+ parser.setLanguage(await languageProfile.loadLanguage(extension));
15
+ return parser;
16
+ }
17
+
18
+ async function getParserForPath(path: string, parsers: Map<string, Parser>) {
19
+ const extension = extname(path).toLowerCase();
20
+ const parser = parsers.get(extension);
21
+ if (parser) return parser;
22
+
23
+ const createdParser = await createCodeParser(extension);
24
+ parsers.set(extension, createdParser);
25
+ return createdParser;
26
+ }
27
+
28
+ async function getRootNode(cwd: string, path: string) {
29
+ const parsers = new Map<string, Parser>();
30
+ const absolutePath = resolve(cwd, path);
31
+
32
+ let code = "";
33
+ try {
34
+ code = await readFile(absolutePath, "utf8");
35
+ } catch {
36
+ return;
37
+ }
38
+
39
+ const parser = await getParserForPath(path, parsers);
40
+ const tree = parser.parse(code);
41
+ return tree.rootNode;
42
+ }
43
+
44
+ export async function collectSymbols(cwd: string, path: string, kinds: Set<SymbolKind>) {
45
+ const extension = extname(path).toLowerCase();
46
+ const profile = getLanguageProfile(extension);
47
+ if (!profile) return [];
48
+
49
+ const root = await getRootNode(cwd, path);
50
+ if (!root) return [];
51
+
52
+ const symbols: LanguageSymbol[] = [];
53
+ const pendingNodes: SyntaxNode[] = [root];
54
+ const symbolIdCounts = new Map<string, number>();
55
+
56
+ while (pendingNodes.length > 0) {
57
+ const currentNode = pendingNodes.pop();
58
+ if (!currentNode) continue;
59
+
60
+ for (let childIndex = currentNode.namedChildCount - 1; childIndex >= 0; childIndex -= 1) {
61
+ const namedChild = currentNode.namedChild(childIndex);
62
+ if (namedChild) {
63
+ pendingNodes.push(namedChild);
64
+ }
65
+ }
66
+
67
+ const symbolKind = profile.resolveSymbolKind(currentNode);
68
+ if (!symbolKind || !kinds.has(symbolKind) || profile.shouldSkipSymbol(currentNode)) {
69
+ continue;
70
+ }
71
+
72
+ const baseName = profile.readNodeName(currentNode);
73
+ if (!baseName) continue;
74
+
75
+ const containerName = profile.readContainerName(currentNode);
76
+ const symbolName =
77
+ symbolKind === "method" && containerName ? `${containerName}.${baseName}` : baseName;
78
+
79
+ const symbolIdCount = (symbolIdCounts.get(symbolName) ?? 0) + 1;
80
+ symbolIdCounts.set(symbolName, symbolIdCount);
81
+ const isAnonymous = symbolName === "anonymous" && !currentNode.childForFieldName("name");
82
+
83
+ symbols.push({
84
+ name:
85
+ isAnonymous || symbolIdCount > 1 ? `${symbolName}~${symbolIdCount}` : symbolName,
86
+ path,
87
+ kind: symbolKind,
88
+ node: currentNode,
89
+ range: [currentNode.startPosition.row + 1, currentNode.endPosition.row + 1],
90
+ public: profile.isPublicSymbol(currentNode),
91
+ });
92
+ }
93
+
94
+ return symbols;
95
+ }
@@ -0,0 +1,23 @@
1
+ declare module "tree-sitter-go" {
2
+ import type { Language } from "tree-sitter";
3
+ const language: Language;
4
+ export default language;
5
+ }
6
+
7
+ declare module "tree-sitter-java" {
8
+ import type { Language } from "tree-sitter";
9
+ const language: Language;
10
+ export default language;
11
+ }
12
+
13
+ declare module "tree-sitter-python" {
14
+ import type { Language } from "tree-sitter";
15
+ const language: Language;
16
+ export default language;
17
+ }
18
+
19
+ declare module "tree-sitter-rust" {
20
+ import type { Language } from "tree-sitter";
21
+ const language: Language;
22
+ export default language;
23
+ }