fraude-code 0.1.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 +68 -0
- package/dist/index.js +179297 -0
- package/package.json +88 -0
- package/src/agent/agent.ts +475 -0
- package/src/agent/contextManager.ts +141 -0
- package/src/agent/index.ts +14 -0
- package/src/agent/pendingChanges.ts +270 -0
- package/src/agent/prompts/AskPrompt.txt +10 -0
- package/src/agent/prompts/FastPrompt.txt +40 -0
- package/src/agent/prompts/PlannerPrompt.txt +51 -0
- package/src/agent/prompts/ReviewerPrompt.txt +57 -0
- package/src/agent/prompts/WorkerPrompt.txt +33 -0
- package/src/agent/subagents/askAgent.ts +37 -0
- package/src/agent/subagents/extractionAgent.ts +123 -0
- package/src/agent/subagents/fastAgent.ts +45 -0
- package/src/agent/subagents/managerAgent.ts +36 -0
- package/src/agent/subagents/relationAgent.ts +76 -0
- package/src/agent/subagents/researchSubAgent.ts +79 -0
- package/src/agent/subagents/reviewerSubAgent.ts +42 -0
- package/src/agent/subagents/workerSubAgent.ts +42 -0
- package/src/agent/tools/bashTool.ts +94 -0
- package/src/agent/tools/descriptions/bash.txt +47 -0
- package/src/agent/tools/descriptions/edit.txt +7 -0
- package/src/agent/tools/descriptions/glob.txt +4 -0
- package/src/agent/tools/descriptions/grep.txt +8 -0
- package/src/agent/tools/descriptions/lsp.txt +20 -0
- package/src/agent/tools/descriptions/plan.txt +3 -0
- package/src/agent/tools/descriptions/read.txt +9 -0
- package/src/agent/tools/descriptions/todo.txt +12 -0
- package/src/agent/tools/descriptions/write.txt +8 -0
- package/src/agent/tools/editTool.ts +44 -0
- package/src/agent/tools/globTool.ts +59 -0
- package/src/agent/tools/grepTool.ts +343 -0
- package/src/agent/tools/lspTool.ts +429 -0
- package/src/agent/tools/planTool.ts +118 -0
- package/src/agent/tools/readTool.ts +78 -0
- package/src/agent/tools/rememberTool.ts +91 -0
- package/src/agent/tools/testRunnerTool.ts +77 -0
- package/src/agent/tools/testTool.ts +44 -0
- package/src/agent/tools/todoTool.ts +224 -0
- package/src/agent/tools/writeTool.ts +33 -0
- package/src/commands/COMMANDS.ts +38 -0
- package/src/commands/cerebras/auth.ts +27 -0
- package/src/commands/cerebras/index.ts +31 -0
- package/src/commands/forget.ts +29 -0
- package/src/commands/google/auth.ts +24 -0
- package/src/commands/google/index.ts +31 -0
- package/src/commands/groq/add_model.ts +60 -0
- package/src/commands/groq/auth.ts +24 -0
- package/src/commands/groq/index.ts +33 -0
- package/src/commands/index.ts +65 -0
- package/src/commands/knowledge.ts +92 -0
- package/src/commands/log.ts +32 -0
- package/src/commands/mistral/auth.ts +27 -0
- package/src/commands/mistral/index.ts +31 -0
- package/src/commands/model/index.ts +145 -0
- package/src/commands/models/index.ts +16 -0
- package/src/commands/ollama/index.ts +29 -0
- package/src/commands/openrouter/add_model.ts +64 -0
- package/src/commands/openrouter/auth.ts +24 -0
- package/src/commands/openrouter/index.ts +33 -0
- package/src/commands/remember.ts +48 -0
- package/src/commands/serve.ts +31 -0
- package/src/commands/session/index.ts +21 -0
- package/src/commands/usage.ts +15 -0
- package/src/commands/visualize.ts +773 -0
- package/src/components/App.tsx +55 -0
- package/src/components/IntroComponent.tsx +70 -0
- package/src/components/LoaderComponent.tsx +68 -0
- package/src/components/OutputRenderer.tsx +88 -0
- package/src/components/SettingsRenderer.tsx +23 -0
- package/src/components/input/CommandSuggestions.tsx +41 -0
- package/src/components/input/FileSuggestions.tsx +61 -0
- package/src/components/input/InputBox.tsx +371 -0
- package/src/components/output/CheckpointView.tsx +13 -0
- package/src/components/output/CommandView.tsx +13 -0
- package/src/components/output/CommentView.tsx +12 -0
- package/src/components/output/ConfirmationView.tsx +179 -0
- package/src/components/output/ContextUsage.tsx +62 -0
- package/src/components/output/DiffView.tsx +202 -0
- package/src/components/output/ErrorView.tsx +14 -0
- package/src/components/output/InteractiveServerView.tsx +69 -0
- package/src/components/output/KnowledgeView.tsx +220 -0
- package/src/components/output/MarkdownView.tsx +15 -0
- package/src/components/output/ModelSelectView.tsx +71 -0
- package/src/components/output/ReasoningView.tsx +21 -0
- package/src/components/output/ToolCallView.tsx +45 -0
- package/src/components/settings/ModelList.tsx +250 -0
- package/src/components/settings/TokenUsage.tsx +274 -0
- package/src/config/schema.ts +19 -0
- package/src/config/settings.ts +229 -0
- package/src/index.tsx +100 -0
- package/src/parsers/tree-sitter-python.wasm +0 -0
- package/src/providers/providers.ts +71 -0
- package/src/services/PluginLoader.ts +123 -0
- package/src/services/cerebras.ts +69 -0
- package/src/services/embeddingService.ts +229 -0
- package/src/services/google.ts +65 -0
- package/src/services/graphSerializer.ts +248 -0
- package/src/services/groq.ts +23 -0
- package/src/services/knowledgeOrchestrator.ts +286 -0
- package/src/services/mistral.ts +79 -0
- package/src/services/ollama.ts +109 -0
- package/src/services/openrouter.ts +23 -0
- package/src/services/symbolExtractor.ts +277 -0
- package/src/store/useFraudeStore.ts +123 -0
- package/src/store/useSettingsStore.ts +38 -0
- package/src/theme.ts +26 -0
- package/src/types/Agent.ts +147 -0
- package/src/types/CommandDefinition.ts +8 -0
- package/src/types/Model.ts +94 -0
- package/src/types/OutputItem.ts +24 -0
- package/src/types/PluginContext.ts +55 -0
- package/src/types/TokenUsage.ts +5 -0
- package/src/types/assets.d.ts +4 -0
- package/src/utils/agentCognition.ts +1152 -0
- package/src/utils/fileSuggestions.ts +111 -0
- package/src/utils/index.ts +17 -0
- package/src/utils/initFraude.ts +8 -0
- package/src/utils/logger.ts +24 -0
- package/src/utils/lspClient.ts +1415 -0
- package/src/utils/paths.ts +24 -0
- package/src/utils/queryHandler.ts +227 -0
- package/src/utils/router.ts +278 -0
- package/src/utils/streamHandler.ts +132 -0
- package/src/utils/treeSitterQueries.ts +125 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,1415 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { Parser, Language, Query } from "web-tree-sitter";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import {
|
|
6
|
+
getQueriesForExtension,
|
|
7
|
+
type LanguageQueries,
|
|
8
|
+
} from "./treeSitterQueries";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A symbol with extended metadata for knowledge graph enrichment.
|
|
12
|
+
*/
|
|
13
|
+
export interface DetailedSymbol {
|
|
14
|
+
name: string;
|
|
15
|
+
kind: string;
|
|
16
|
+
line: number;
|
|
17
|
+
signature?: string; // e.g., "foo(x: number, y: string): boolean"
|
|
18
|
+
docstring?: string; // JSDoc or docstring content
|
|
19
|
+
exported?: boolean; // Is this symbol exported?
|
|
20
|
+
children?: DetailedSymbol[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Common interface for all language providers
|
|
25
|
+
*/
|
|
26
|
+
export interface LanguageProvider {
|
|
27
|
+
isSupported(extension: string): boolean;
|
|
28
|
+
getDiagnostics(
|
|
29
|
+
filePath: string,
|
|
30
|
+
content: string,
|
|
31
|
+
): Promise<{ errors: string[]; warnings: string[] }>;
|
|
32
|
+
findDefinition(
|
|
33
|
+
filePath: string,
|
|
34
|
+
content: string,
|
|
35
|
+
line: number,
|
|
36
|
+
character: number,
|
|
37
|
+
): Promise<{ file: string; line: number; preview?: string } | null>;
|
|
38
|
+
findReferences(
|
|
39
|
+
filePath: string,
|
|
40
|
+
content: string,
|
|
41
|
+
line: number,
|
|
42
|
+
character: number,
|
|
43
|
+
): Promise<Array<{ file: string; line: number }>>;
|
|
44
|
+
getDocumentSymbols(
|
|
45
|
+
filePath: string,
|
|
46
|
+
content: string,
|
|
47
|
+
): Promise<
|
|
48
|
+
Array<{ name: string; kind: string; line: number; children?: any[] }>
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
// Enhanced symbol extraction with signatures and docstrings
|
|
52
|
+
getDocumentSymbolsWithDetails?(
|
|
53
|
+
filePath: string,
|
|
54
|
+
content: string,
|
|
55
|
+
): Promise<DetailedSymbol[]>;
|
|
56
|
+
|
|
57
|
+
// Added missing methods
|
|
58
|
+
getSymbolInfo(
|
|
59
|
+
filePath: string,
|
|
60
|
+
content: string,
|
|
61
|
+
line: number,
|
|
62
|
+
character: number,
|
|
63
|
+
): Promise<string | null>;
|
|
64
|
+
findImplementation(
|
|
65
|
+
filePath: string,
|
|
66
|
+
content: string,
|
|
67
|
+
line: number,
|
|
68
|
+
character: number,
|
|
69
|
+
): Promise<Array<{ file: string; line: number; preview?: string }>>;
|
|
70
|
+
|
|
71
|
+
// Optional advanced features
|
|
72
|
+
prepareCallHierarchy?(
|
|
73
|
+
filePath: string,
|
|
74
|
+
content: string,
|
|
75
|
+
line: number,
|
|
76
|
+
character: number,
|
|
77
|
+
): Promise<Array<{ name: string; kind: string; file: string; line: number }>>;
|
|
78
|
+
getIncomingCalls?(
|
|
79
|
+
filePath: string,
|
|
80
|
+
content: string,
|
|
81
|
+
line: number,
|
|
82
|
+
character: number,
|
|
83
|
+
): Promise<Array<{ name: string; file: string; line: number }>>;
|
|
84
|
+
getOutgoingCalls?(
|
|
85
|
+
filePath: string,
|
|
86
|
+
content: string,
|
|
87
|
+
line: number,
|
|
88
|
+
character: number,
|
|
89
|
+
): Promise<
|
|
90
|
+
Array<{ name: string; file: string; line: number; sourceContext?: string }>
|
|
91
|
+
>;
|
|
92
|
+
searchWorkspaceSymbols?(
|
|
93
|
+
query: string,
|
|
94
|
+
): Promise<Array<{ name: string; kind: string; file: string; line: number }>>;
|
|
95
|
+
|
|
96
|
+
// Import extraction for knowledge graph
|
|
97
|
+
getImports?(
|
|
98
|
+
filePath: string,
|
|
99
|
+
content: string,
|
|
100
|
+
): Promise<Array<{ module: string; alias?: string; isRelative: boolean }>>;
|
|
101
|
+
|
|
102
|
+
// Inheritance extraction for knowledge graph
|
|
103
|
+
getInheritance?(
|
|
104
|
+
filePath: string,
|
|
105
|
+
content: string,
|
|
106
|
+
): Promise<Array<{ className: string; parentName: string; line: number }>>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ----------------------------------------------------------------------
|
|
111
|
+
* TIER 1: TypeScript Provider (High Fidelity)
|
|
112
|
+
* Uses the TypeScript Compiler API to provide rich analysis for TS/JS files.
|
|
113
|
+
* ----------------------------------------------------------------------
|
|
114
|
+
*/
|
|
115
|
+
class TypeScriptProvider implements LanguageProvider {
|
|
116
|
+
private service: ts.LanguageService;
|
|
117
|
+
private files: Map<string, { version: number; content: string }> = new Map();
|
|
118
|
+
|
|
119
|
+
constructor(private rootPath: string) {
|
|
120
|
+
const registry = ts.createDocumentRegistry();
|
|
121
|
+
const serviceHost: ts.LanguageServiceHost = {
|
|
122
|
+
getScriptFileNames: () => Array.from(this.files.keys()),
|
|
123
|
+
getScriptVersion: (fileName) =>
|
|
124
|
+
this.files.get(fileName)?.version.toString() || "0",
|
|
125
|
+
getScriptSnapshot: (fileName) => {
|
|
126
|
+
const file = this.files.get(fileName);
|
|
127
|
+
if (file) {
|
|
128
|
+
return ts.ScriptSnapshot.fromString(file.content);
|
|
129
|
+
}
|
|
130
|
+
if (fs.existsSync(fileName)) {
|
|
131
|
+
return ts.ScriptSnapshot.fromString(
|
|
132
|
+
fs.readFileSync(fileName, "utf-8"),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
},
|
|
137
|
+
getCurrentDirectory: () => this.rootPath,
|
|
138
|
+
getCompilationSettings: () => ({
|
|
139
|
+
target: ts.ScriptTarget.ESNext,
|
|
140
|
+
module: ts.ModuleKind.ESNext,
|
|
141
|
+
allowJs: true,
|
|
142
|
+
jsx: ts.JsxEmit.React,
|
|
143
|
+
strict: true,
|
|
144
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
145
|
+
esModuleInterop: true,
|
|
146
|
+
skipLibCheck: true,
|
|
147
|
+
resolveJsonModule: true,
|
|
148
|
+
}),
|
|
149
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
150
|
+
fileExists: ts.sys.fileExists,
|
|
151
|
+
readFile: ts.sys.readFile,
|
|
152
|
+
readDirectory: ts.sys.readDirectory,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
this.service = ts.createLanguageService(serviceHost, registry);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
isSupported(ext: string): boolean {
|
|
159
|
+
return ["ts", "tsx", "js", "jsx"].includes(ext);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private updateFile(filePath: string, content: string) {
|
|
163
|
+
const current = this.files.get(filePath);
|
|
164
|
+
if (!current || current.content !== content) {
|
|
165
|
+
this.files.set(filePath, {
|
|
166
|
+
version: (current?.version || 0) + 1,
|
|
167
|
+
content,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getDiagnostics(filePath: string, content: string) {
|
|
173
|
+
this.updateFile(filePath, content);
|
|
174
|
+
|
|
175
|
+
const syntactic = this.service.getSyntacticDiagnostics(filePath);
|
|
176
|
+
const semantic = this.service.getSemanticDiagnostics(filePath);
|
|
177
|
+
|
|
178
|
+
const all = [...syntactic, ...semantic];
|
|
179
|
+
const errors: string[] = [];
|
|
180
|
+
const warnings: string[] = [];
|
|
181
|
+
|
|
182
|
+
for (const d of all) {
|
|
183
|
+
const line = d.file
|
|
184
|
+
? d.file.getLineAndCharacterOfPosition(d.start!).line + 1
|
|
185
|
+
: 0;
|
|
186
|
+
const msg = ts.flattenDiagnosticMessageText(d.messageText, "\n");
|
|
187
|
+
const fmt = `Line ${line}: ${msg}`;
|
|
188
|
+
if (d.category === ts.DiagnosticCategory.Error) {
|
|
189
|
+
errors.push(fmt);
|
|
190
|
+
} else {
|
|
191
|
+
warnings.push(fmt);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { errors, warnings };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async findDefinition(
|
|
199
|
+
filePath: string,
|
|
200
|
+
content: string,
|
|
201
|
+
line: number,
|
|
202
|
+
character: number,
|
|
203
|
+
) {
|
|
204
|
+
this.updateFile(filePath, content);
|
|
205
|
+
const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
|
|
206
|
+
if (!sourceFile) return null;
|
|
207
|
+
|
|
208
|
+
const pos = sourceFile.getPositionOfLineAndCharacter(
|
|
209
|
+
line - 1,
|
|
210
|
+
character - 1,
|
|
211
|
+
);
|
|
212
|
+
const defs = this.service.getDefinitionAtPosition(filePath, pos);
|
|
213
|
+
|
|
214
|
+
if (!defs || defs.length === 0) return null;
|
|
215
|
+
const def = defs[0];
|
|
216
|
+
if (!def) return null;
|
|
217
|
+
|
|
218
|
+
const defFile = def.fileName;
|
|
219
|
+
const defStart = def.textSpan.start;
|
|
220
|
+
|
|
221
|
+
let fileContent = "";
|
|
222
|
+
if (this.files.has(defFile)) {
|
|
223
|
+
fileContent = this.files.get(defFile)!.content;
|
|
224
|
+
} else if (fs.existsSync(defFile)) {
|
|
225
|
+
fileContent = fs.readFileSync(defFile, "utf-8");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!fileContent) return null;
|
|
229
|
+
|
|
230
|
+
const tempSource = ts.createSourceFile(
|
|
231
|
+
defFile,
|
|
232
|
+
fileContent,
|
|
233
|
+
ts.ScriptTarget.Latest,
|
|
234
|
+
);
|
|
235
|
+
const linePos = tempSource.getLineAndCharacterOfPosition(defStart);
|
|
236
|
+
|
|
237
|
+
const lines = fileContent.split("\n");
|
|
238
|
+
const preview = lines
|
|
239
|
+
.slice(Math.max(0, linePos.line - 1), linePos.line + 2)
|
|
240
|
+
.join("\n");
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
file: defFile,
|
|
244
|
+
line: linePos.line + 1,
|
|
245
|
+
preview,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async getSymbolInfo(
|
|
250
|
+
filePath: string,
|
|
251
|
+
content: string,
|
|
252
|
+
line: number,
|
|
253
|
+
character: number,
|
|
254
|
+
) {
|
|
255
|
+
this.updateFile(filePath, content);
|
|
256
|
+
const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
|
|
257
|
+
if (!sourceFile) return null;
|
|
258
|
+
|
|
259
|
+
const pos = sourceFile.getPositionOfLineAndCharacter(
|
|
260
|
+
line - 1,
|
|
261
|
+
character - 1,
|
|
262
|
+
);
|
|
263
|
+
const info = this.service.getQuickInfoAtPosition(filePath, pos);
|
|
264
|
+
|
|
265
|
+
if (!info) return null;
|
|
266
|
+
|
|
267
|
+
const displayParts = ts.displayPartsToString(info.displayParts);
|
|
268
|
+
const doc = ts.displayPartsToString(info.documentation);
|
|
269
|
+
|
|
270
|
+
return `${displayParts}\n${doc}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async findImplementation(
|
|
274
|
+
filePath: string,
|
|
275
|
+
content: string,
|
|
276
|
+
line: number,
|
|
277
|
+
character: number,
|
|
278
|
+
) {
|
|
279
|
+
this.updateFile(filePath, content);
|
|
280
|
+
const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
|
|
281
|
+
if (!sourceFile) return [];
|
|
282
|
+
|
|
283
|
+
const pos = sourceFile.getPositionOfLineAndCharacter(
|
|
284
|
+
line - 1,
|
|
285
|
+
character - 1,
|
|
286
|
+
);
|
|
287
|
+
const impls = this.service.getImplementationAtPosition(filePath, pos);
|
|
288
|
+
|
|
289
|
+
if (!impls) return [];
|
|
290
|
+
|
|
291
|
+
return impls.map((impl) => {
|
|
292
|
+
const implSource = this.service
|
|
293
|
+
.getProgram()
|
|
294
|
+
?.getSourceFile(impl.fileName);
|
|
295
|
+
const line = implSource
|
|
296
|
+
? implSource.getLineAndCharacterOfPosition(impl.textSpan.start).line + 1
|
|
297
|
+
: 1;
|
|
298
|
+
return { file: impl.fileName, line };
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async findReferences(
|
|
303
|
+
filePath: string,
|
|
304
|
+
content: string,
|
|
305
|
+
line: number,
|
|
306
|
+
character: number,
|
|
307
|
+
) {
|
|
308
|
+
this.updateFile(filePath, content);
|
|
309
|
+
const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
|
|
310
|
+
if (!sourceFile) return [];
|
|
311
|
+
|
|
312
|
+
const pos = sourceFile.getPositionOfLineAndCharacter(
|
|
313
|
+
line - 1,
|
|
314
|
+
character - 1,
|
|
315
|
+
);
|
|
316
|
+
const refs = this.service.getReferencesAtPosition(filePath, pos);
|
|
317
|
+
|
|
318
|
+
if (!refs) return [];
|
|
319
|
+
|
|
320
|
+
return refs.map((ref) => {
|
|
321
|
+
const refSource = this.service.getProgram()?.getSourceFile(ref.fileName);
|
|
322
|
+
const line = refSource
|
|
323
|
+
? refSource.getLineAndCharacterOfPosition(ref.textSpan.start).line + 1
|
|
324
|
+
: 1;
|
|
325
|
+
return { file: ref.fileName, line };
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async getDocumentSymbols(filePath: string, content: string) {
|
|
330
|
+
this.updateFile(filePath, content);
|
|
331
|
+
const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
|
|
332
|
+
if (!sourceFile) return [];
|
|
333
|
+
|
|
334
|
+
const symbols: any[] = [];
|
|
335
|
+
|
|
336
|
+
const visit = (node: ts.Node) => {
|
|
337
|
+
let name = "";
|
|
338
|
+
let kind = "";
|
|
339
|
+
let children: any[] = [];
|
|
340
|
+
|
|
341
|
+
// Determine node type and name
|
|
342
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
343
|
+
name = node.name.text;
|
|
344
|
+
kind = "Function";
|
|
345
|
+
// Recurse for local functions/vars if needed, but usually flat is okay for top level
|
|
346
|
+
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
347
|
+
name = node.name.text;
|
|
348
|
+
kind = "Class";
|
|
349
|
+
// Visit children (methods)
|
|
350
|
+
ts.forEachChild(node, (child) => {
|
|
351
|
+
if (ts.isMethodDeclaration(child) && ts.isIdentifier(child.name)) {
|
|
352
|
+
const line =
|
|
353
|
+
sourceFile.getLineAndCharacterOfPosition(child.getStart()).line +
|
|
354
|
+
1;
|
|
355
|
+
children.push({ name: child.name.text, kind: "Method", line });
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
} else if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
359
|
+
name = node.name.text;
|
|
360
|
+
kind = "Interface";
|
|
361
|
+
} else if (ts.isVariableStatement(node)) {
|
|
362
|
+
// Handle exposed const/let
|
|
363
|
+
const list = node.declarationList;
|
|
364
|
+
for (const decl of list.declarations) {
|
|
365
|
+
if (ts.isIdentifier(decl.name)) {
|
|
366
|
+
name = decl.name.text;
|
|
367
|
+
kind = "Variable";
|
|
368
|
+
// Note: handling only the first one if multiple allowed, but structure implies loop
|
|
369
|
+
// We'll just push directly here
|
|
370
|
+
const line =
|
|
371
|
+
sourceFile.getLineAndCharacterOfPosition(decl.getStart()).line +
|
|
372
|
+
1;
|
|
373
|
+
symbols.push({ name, kind, line });
|
|
374
|
+
name = ""; // reset so we don't push again at end
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (name) {
|
|
380
|
+
const line =
|
|
381
|
+
sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
382
|
+
symbols.push({
|
|
383
|
+
name,
|
|
384
|
+
kind,
|
|
385
|
+
line,
|
|
386
|
+
children: children.length > 0 ? children : undefined,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
ts.forEachChild(sourceFile, visit);
|
|
392
|
+
return symbols;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async getDocumentSymbolsWithDetails(
|
|
396
|
+
filePath: string,
|
|
397
|
+
content: string,
|
|
398
|
+
): Promise<import("./lspClient").DetailedSymbol[]> {
|
|
399
|
+
this.updateFile(filePath, content);
|
|
400
|
+
const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
|
|
401
|
+
if (!sourceFile) return [];
|
|
402
|
+
|
|
403
|
+
const typeChecker = this.service.getProgram()?.getTypeChecker();
|
|
404
|
+
const symbols: import("./lspClient").DetailedSymbol[] = [];
|
|
405
|
+
|
|
406
|
+
const getJSDocComment = (node: ts.Node): string | undefined => {
|
|
407
|
+
const jsDocs = ts.getJSDocCommentsAndTags(node);
|
|
408
|
+
if (jsDocs.length === 0) return undefined;
|
|
409
|
+
const comments: string[] = [];
|
|
410
|
+
for (const doc of jsDocs) {
|
|
411
|
+
if (ts.isJSDoc(doc) && doc.comment) {
|
|
412
|
+
comments.push(
|
|
413
|
+
typeof doc.comment === "string"
|
|
414
|
+
? doc.comment
|
|
415
|
+
: doc.comment.map((c) => c.text).join(""),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return comments.length > 0 ? comments.join("\n") : undefined;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const hasExportModifier = (node: ts.Node): boolean => {
|
|
423
|
+
if (!ts.canHaveModifiers(node)) return false;
|
|
424
|
+
const modifiers = ts.getModifiers(node);
|
|
425
|
+
if (!modifiers) return false;
|
|
426
|
+
return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const getFunctionSignature = (
|
|
430
|
+
node: ts.FunctionDeclaration | ts.MethodDeclaration | ts.ArrowFunction,
|
|
431
|
+
): string | undefined => {
|
|
432
|
+
if (!typeChecker) return undefined;
|
|
433
|
+
try {
|
|
434
|
+
const signature = typeChecker.getSignatureFromDeclaration(node);
|
|
435
|
+
if (signature) {
|
|
436
|
+
return typeChecker.signatureToString(
|
|
437
|
+
signature,
|
|
438
|
+
node,
|
|
439
|
+
ts.TypeFormatFlags.WriteArrowStyleSignature,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
// Fallback: build manually
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const visit = (node: ts.Node) => {
|
|
449
|
+
let sym: import("./lspClient").DetailedSymbol | null = null;
|
|
450
|
+
|
|
451
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
452
|
+
sym = {
|
|
453
|
+
name: node.name.text,
|
|
454
|
+
kind: "Function",
|
|
455
|
+
line:
|
|
456
|
+
sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
457
|
+
signature: getFunctionSignature(node),
|
|
458
|
+
docstring: getJSDocComment(node),
|
|
459
|
+
exported: hasExportModifier(node),
|
|
460
|
+
};
|
|
461
|
+
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
462
|
+
const children: import("./lspClient").DetailedSymbol[] = [];
|
|
463
|
+
ts.forEachChild(node, (child) => {
|
|
464
|
+
if (ts.isMethodDeclaration(child) && ts.isIdentifier(child.name)) {
|
|
465
|
+
children.push({
|
|
466
|
+
name: child.name.text,
|
|
467
|
+
kind: "Method",
|
|
468
|
+
line:
|
|
469
|
+
sourceFile.getLineAndCharacterOfPosition(child.getStart())
|
|
470
|
+
.line + 1,
|
|
471
|
+
signature: getFunctionSignature(child),
|
|
472
|
+
docstring: getJSDocComment(child),
|
|
473
|
+
});
|
|
474
|
+
} else if (
|
|
475
|
+
ts.isPropertyDeclaration(child) &&
|
|
476
|
+
ts.isIdentifier(child.name)
|
|
477
|
+
) {
|
|
478
|
+
children.push({
|
|
479
|
+
name: child.name.text,
|
|
480
|
+
kind: "Property",
|
|
481
|
+
line:
|
|
482
|
+
sourceFile.getLineAndCharacterOfPosition(child.getStart())
|
|
483
|
+
.line + 1,
|
|
484
|
+
docstring: getJSDocComment(child),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
sym = {
|
|
489
|
+
name: node.name.text,
|
|
490
|
+
kind: "Class",
|
|
491
|
+
line:
|
|
492
|
+
sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
493
|
+
docstring: getJSDocComment(node),
|
|
494
|
+
exported: hasExportModifier(node),
|
|
495
|
+
children: children.length > 0 ? children : undefined,
|
|
496
|
+
};
|
|
497
|
+
} else if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
498
|
+
sym = {
|
|
499
|
+
name: node.name.text,
|
|
500
|
+
kind: "Interface",
|
|
501
|
+
line:
|
|
502
|
+
sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
503
|
+
docstring: getJSDocComment(node),
|
|
504
|
+
exported: hasExportModifier(node),
|
|
505
|
+
};
|
|
506
|
+
} else if (ts.isVariableStatement(node)) {
|
|
507
|
+
const isExported = hasExportModifier(node);
|
|
508
|
+
for (const decl of node.declarationList.declarations) {
|
|
509
|
+
if (ts.isIdentifier(decl.name)) {
|
|
510
|
+
// Check if it's an arrow function assigned to const
|
|
511
|
+
let signature: string | undefined;
|
|
512
|
+
if (decl.initializer && ts.isArrowFunction(decl.initializer)) {
|
|
513
|
+
signature = getFunctionSignature(decl.initializer);
|
|
514
|
+
}
|
|
515
|
+
symbols.push({
|
|
516
|
+
name: decl.name.text,
|
|
517
|
+
kind: signature ? "Function" : "Variable",
|
|
518
|
+
line:
|
|
519
|
+
sourceFile.getLineAndCharacterOfPosition(decl.getStart()).line +
|
|
520
|
+
1,
|
|
521
|
+
signature,
|
|
522
|
+
docstring: getJSDocComment(node),
|
|
523
|
+
exported: isExported,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (sym) {
|
|
530
|
+
symbols.push(sym);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
ts.forEachChild(sourceFile, visit);
|
|
535
|
+
return symbols;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async prepareCallHierarchy(
|
|
539
|
+
filePath: string,
|
|
540
|
+
content: string,
|
|
541
|
+
line: number,
|
|
542
|
+
character: number,
|
|
543
|
+
) {
|
|
544
|
+
this.updateFile(filePath, content);
|
|
545
|
+
const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
|
|
546
|
+
if (!sourceFile) return [];
|
|
547
|
+
|
|
548
|
+
const pos = sourceFile.getPositionOfLineAndCharacter(
|
|
549
|
+
line - 1,
|
|
550
|
+
character - 1,
|
|
551
|
+
);
|
|
552
|
+
// TypeScript service doesn't have a direct 'prepareCallHierarchy' - we simulate it
|
|
553
|
+
// by finding the symbol at position and returning it as a CallHierarchyItem
|
|
554
|
+
const defs = this.service.getDefinitionAtPosition(filePath, pos);
|
|
555
|
+
if (!defs || defs.length === 0) return [];
|
|
556
|
+
|
|
557
|
+
const def = defs[0];
|
|
558
|
+
if (!def) return [];
|
|
559
|
+
|
|
560
|
+
const defSource = this.service.getProgram()?.getSourceFile(def.fileName);
|
|
561
|
+
const defLine = defSource
|
|
562
|
+
? defSource.getLineAndCharacterOfPosition(def.textSpan.start).line + 1
|
|
563
|
+
: 1;
|
|
564
|
+
|
|
565
|
+
return [
|
|
566
|
+
{
|
|
567
|
+
name: def.name,
|
|
568
|
+
kind: def.kind,
|
|
569
|
+
file: def.fileName,
|
|
570
|
+
line: defLine,
|
|
571
|
+
},
|
|
572
|
+
];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async getOutgoingCalls(
|
|
576
|
+
filePath: string,
|
|
577
|
+
content: string,
|
|
578
|
+
line: number,
|
|
579
|
+
character: number,
|
|
580
|
+
) {
|
|
581
|
+
this.updateFile(filePath, content);
|
|
582
|
+
// Note: TS LanguageService doesn't expose provideCallHierarchyOutgoingCalls directly
|
|
583
|
+
// widely available. We'll use a simplified AST traversal approach for now
|
|
584
|
+
// to find calls within the function body at the given position.
|
|
585
|
+
|
|
586
|
+
const sourceFile = this.service.getProgram()?.getSourceFile(filePath);
|
|
587
|
+
if (!sourceFile) return [];
|
|
588
|
+
|
|
589
|
+
const pos = sourceFile.getPositionOfLineAndCharacter(
|
|
590
|
+
line - 1,
|
|
591
|
+
character - 1,
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
// Find the function-like node containing the cursor
|
|
595
|
+
let node = this.findContainerNode(sourceFile, pos);
|
|
596
|
+
if (!node) return [];
|
|
597
|
+
|
|
598
|
+
const calls: Array<{ name: string; file: string; line: number }> = [];
|
|
599
|
+
|
|
600
|
+
const visit = (n: ts.Node) => {
|
|
601
|
+
if (ts.isCallExpression(n)) {
|
|
602
|
+
// Resolve the called symbol
|
|
603
|
+
const typeChecker = this.service.getProgram()?.getTypeChecker();
|
|
604
|
+
// Note: Using private access to program/typechecker is tricky,
|
|
605
|
+
// we'll try to find definition of the expression
|
|
606
|
+
const exprStart = n.expression.getStart();
|
|
607
|
+
const defs = this.service.getDefinitionAtPosition(filePath, exprStart);
|
|
608
|
+
|
|
609
|
+
if (defs && defs.length > 0) {
|
|
610
|
+
const def = defs[0];
|
|
611
|
+
if (def) {
|
|
612
|
+
const defSource = this.service
|
|
613
|
+
.getProgram()
|
|
614
|
+
?.getSourceFile(def.fileName);
|
|
615
|
+
const defLine = defSource
|
|
616
|
+
? defSource.getLineAndCharacterOfPosition(def.textSpan.start)
|
|
617
|
+
.line + 1
|
|
618
|
+
: 1;
|
|
619
|
+
|
|
620
|
+
calls.push({
|
|
621
|
+
name: def.name,
|
|
622
|
+
file: def.fileName,
|
|
623
|
+
line: defLine,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
ts.forEachChild(n, visit);
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
visit(node);
|
|
632
|
+
return calls;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private findContainerNode(
|
|
636
|
+
sourceFile: ts.SourceFile,
|
|
637
|
+
pos: number,
|
|
638
|
+
): ts.Node | null {
|
|
639
|
+
let fond: ts.Node | null = null;
|
|
640
|
+
|
|
641
|
+
const visit = (node: ts.Node) => {
|
|
642
|
+
if (node.pos <= pos && node.end >= pos) {
|
|
643
|
+
if (
|
|
644
|
+
ts.isFunctionDeclaration(node) ||
|
|
645
|
+
ts.isMethodDeclaration(node) ||
|
|
646
|
+
ts.isArrowFunction(node) ||
|
|
647
|
+
ts.isFunctionExpression(node)
|
|
648
|
+
) {
|
|
649
|
+
fond = node;
|
|
650
|
+
}
|
|
651
|
+
ts.forEachChild(node, visit);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
ts.forEachChild(sourceFile, visit);
|
|
656
|
+
return fond;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* ----------------------------------------------------------------------
|
|
662
|
+
* TIER 2: Tree-Sitter Provider (Structure & Symbols)
|
|
663
|
+
* ----------------------------------------------------------------------
|
|
664
|
+
*/
|
|
665
|
+
class TreeSitterProvider implements LanguageProvider {
|
|
666
|
+
private parser: any = null;
|
|
667
|
+
private lang: any = null;
|
|
668
|
+
private isReady = false;
|
|
669
|
+
|
|
670
|
+
private initPromise: Promise<void> | null = null;
|
|
671
|
+
|
|
672
|
+
constructor(
|
|
673
|
+
private languageName: string,
|
|
674
|
+
private wasmPath: string,
|
|
675
|
+
) {
|
|
676
|
+
this.initPromise = this.init();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private async init() {
|
|
680
|
+
try {
|
|
681
|
+
if (!fs.existsSync(this.wasmPath)) {
|
|
682
|
+
console.error(`[TreeSitter] WASM file not found: ${this.wasmPath}`);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
await Parser.init();
|
|
686
|
+
this.lang = await Language.load(this.wasmPath);
|
|
687
|
+
this.parser = new Parser();
|
|
688
|
+
this.parser.setLanguage(this.lang);
|
|
689
|
+
this.isReady = true;
|
|
690
|
+
} catch (e) {
|
|
691
|
+
console.error(`[TreeSitter] Init failed for ${this.languageName}:`, e);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Ensure initialization is complete before using the parser.
|
|
697
|
+
*/
|
|
698
|
+
async ensureReady(): Promise<boolean> {
|
|
699
|
+
if (this.initPromise) {
|
|
700
|
+
await this.initPromise;
|
|
701
|
+
}
|
|
702
|
+
return this.isReady;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
isSupported(ext: string): boolean {
|
|
706
|
+
return this.isReady;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async getDiagnostics() {
|
|
710
|
+
return { errors: [], warnings: [] };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async findDefinition(
|
|
714
|
+
filePath: string,
|
|
715
|
+
content: string,
|
|
716
|
+
line: number,
|
|
717
|
+
character: number,
|
|
718
|
+
) {
|
|
719
|
+
if (!this.parser || !this.lang) return null;
|
|
720
|
+
const tree = this.parser.parse(content);
|
|
721
|
+
|
|
722
|
+
// Simple heuristic: search for definition of symbol at cursor
|
|
723
|
+
// Need exact logic for determining symbol at cursor for TS (row/col)
|
|
724
|
+
// Assume we can get the text
|
|
725
|
+
const lines = content.split("\n");
|
|
726
|
+
const docLine = lines[line - 1] || "";
|
|
727
|
+
// Crude extraction of word
|
|
728
|
+
const match = docLine.slice(0, character).match(/[a-zA-Z0-9_]+$/);
|
|
729
|
+
const suffix = docLine.slice(character).match(/^[a-zA-Z0-9_]+/);
|
|
730
|
+
const word = (match ? match[0] : "") + (suffix ? suffix[0] : "");
|
|
731
|
+
if (!word) return null;
|
|
732
|
+
|
|
733
|
+
const symbols = await this.getDocumentSymbols(filePath, content);
|
|
734
|
+
const found = this.findSymbolRecursive(symbols, word);
|
|
735
|
+
if (found) {
|
|
736
|
+
const preview = lines
|
|
737
|
+
.slice(Math.max(0, found.line - 1), found.line + 2)
|
|
738
|
+
.join("\n");
|
|
739
|
+
return { file: filePath, line: found.line, preview };
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private findSymbolRecursive(symbols: any[], name: string): any {
|
|
745
|
+
for (const s of symbols) {
|
|
746
|
+
if (s.name === name) return s;
|
|
747
|
+
if (s.children) {
|
|
748
|
+
const found = this.findSymbolRecursive(s.children, name);
|
|
749
|
+
if (found) return found;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async getSymbolInfo(
|
|
756
|
+
filePath: string,
|
|
757
|
+
content: string,
|
|
758
|
+
line: number,
|
|
759
|
+
character: number,
|
|
760
|
+
) {
|
|
761
|
+
const def = await this.findDefinition(filePath, content, line, character);
|
|
762
|
+
if (def && def.preview) {
|
|
763
|
+
return `Definition:\n${def.preview}`;
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async findImplementation(
|
|
769
|
+
filePath: string,
|
|
770
|
+
content: string,
|
|
771
|
+
line: number,
|
|
772
|
+
character: number,
|
|
773
|
+
) {
|
|
774
|
+
return []; // Not implemented for TreeSitter yet
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async findReferences() {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async getDocumentSymbols(filePath: string, content: string) {
|
|
782
|
+
if (!this.parser || !this.lang) return [];
|
|
783
|
+
const tree = this.parser.parse(content);
|
|
784
|
+
|
|
785
|
+
const ext = filePath.split(".").pop() || "";
|
|
786
|
+
const queries = getQueriesForExtension(ext);
|
|
787
|
+
if (!queries) return [];
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
const query = new Query(this.lang, queries.definitions);
|
|
791
|
+
const matches = query.matches(tree.rootNode);
|
|
792
|
+
|
|
793
|
+
const symbols = matches.map((m: any) => {
|
|
794
|
+
const nameNode = m.captures.find((c: any) => c.name === "name")?.node;
|
|
795
|
+
const defNode = m.captures.find((c: any) => c.name === "def")?.node;
|
|
796
|
+
|
|
797
|
+
// Determine kind based on node type
|
|
798
|
+
let kind = "Function";
|
|
799
|
+
if (defNode?.type.includes("class")) kind = "Class";
|
|
800
|
+
else if (defNode?.type.includes("struct")) kind = "Struct";
|
|
801
|
+
else if (defNode?.type.includes("enum")) kind = "Enum";
|
|
802
|
+
else if (defNode?.type.includes("trait")) kind = "Trait";
|
|
803
|
+
else if (defNode?.type.includes("interface")) kind = "Interface";
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
name: nameNode?.text || "anonymous",
|
|
807
|
+
kind,
|
|
808
|
+
line: (defNode?.startPosition.row || 0) + 1,
|
|
809
|
+
endLine: (defNode?.endPosition.row || 0) + 1,
|
|
810
|
+
};
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
return this.buildHierarchy(
|
|
814
|
+
symbols.sort((a: any, b: any) => a.line - b.line),
|
|
815
|
+
);
|
|
816
|
+
} catch {
|
|
817
|
+
return [];
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Build hierarchical symbol tree from flat list based on line ranges.
|
|
823
|
+
*/
|
|
824
|
+
private buildHierarchy(flatSymbols: any[]): any[] {
|
|
825
|
+
const root: any[] = [];
|
|
826
|
+
const stack: any[] = [];
|
|
827
|
+
|
|
828
|
+
for (const sym of flatSymbols) {
|
|
829
|
+
// Pop items from stack that don't contain this symbol
|
|
830
|
+
while (
|
|
831
|
+
stack.length > 0 &&
|
|
832
|
+
(stack[stack.length - 1].endLine < sym.line ||
|
|
833
|
+
stack[stack.length - 1].endLine < sym.endLine) // Ensure proper nesting
|
|
834
|
+
) {
|
|
835
|
+
stack.pop();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (stack.length === 0) {
|
|
839
|
+
root.push(sym);
|
|
840
|
+
} else {
|
|
841
|
+
const parent = stack[stack.length - 1];
|
|
842
|
+
if (!parent.children) parent.children = [];
|
|
843
|
+
parent.children.push(sym);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Push current symbol to stack if it can have children (classes, etc)
|
|
847
|
+
// Function definitions can also have local functions
|
|
848
|
+
stack.push(sym);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return root;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Collect all nodes of specified types from the AST tree.
|
|
856
|
+
* This is the proven approach from analyzer.ts.
|
|
857
|
+
*/
|
|
858
|
+
private collectNodes(node: any, wantedTypes: Set<string>): any[] {
|
|
859
|
+
const nodes: any[] = [];
|
|
860
|
+
if (wantedTypes.has(node.type)) {
|
|
861
|
+
nodes.push(node);
|
|
862
|
+
}
|
|
863
|
+
for (const child of node.children || []) {
|
|
864
|
+
if (child !== null) {
|
|
865
|
+
nodes.push(...this.collectNodes(child, wantedTypes));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return nodes;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Find the nearest parent node of specified types.
|
|
873
|
+
*/
|
|
874
|
+
private findParent(node: any, wantedTypes: Set<string>): any | null {
|
|
875
|
+
let curr = node.parent;
|
|
876
|
+
while (curr) {
|
|
877
|
+
if (wantedTypes.has(curr.type)) {
|
|
878
|
+
return curr;
|
|
879
|
+
}
|
|
880
|
+
curr = curr.parent;
|
|
881
|
+
}
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async getImports(
|
|
886
|
+
filePath: string,
|
|
887
|
+
content: string,
|
|
888
|
+
): Promise<Array<{ module: string; alias?: string; isRelative: boolean }>> {
|
|
889
|
+
if (!this.parser || !this.lang) return [];
|
|
890
|
+
const tree = this.parser.parse(content);
|
|
891
|
+
if (!tree) return [];
|
|
892
|
+
|
|
893
|
+
const imports: Array<{
|
|
894
|
+
module: string;
|
|
895
|
+
alias?: string;
|
|
896
|
+
isRelative: boolean;
|
|
897
|
+
}> = [];
|
|
898
|
+
|
|
899
|
+
// Python imports - using AST walking like analyzer.ts
|
|
900
|
+
if (this.languageName === "python") {
|
|
901
|
+
const importNodes = this.collectNodes(
|
|
902
|
+
tree.rootNode,
|
|
903
|
+
new Set(["import_statement", "import_from_statement"]),
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
for (const node of importNodes) {
|
|
907
|
+
if (node.type === "import_statement") {
|
|
908
|
+
// import foo, import foo as bar
|
|
909
|
+
const dottedNames = node.descendantsOfType?.("dotted_name") || [];
|
|
910
|
+
for (const n of dottedNames) {
|
|
911
|
+
if (n?.text) {
|
|
912
|
+
imports.push({ module: n.text, isRelative: false });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const aliased = node.descendantsOfType?.("aliased_import") || [];
|
|
916
|
+
for (const n of aliased) {
|
|
917
|
+
const mod = n.child?.(0)?.text;
|
|
918
|
+
const alias = n.child?.(2)?.text;
|
|
919
|
+
if (mod) {
|
|
920
|
+
imports.push({ module: mod, alias, isRelative: false });
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
} else if (node.type === "import_from_statement") {
|
|
924
|
+
// from foo import bar
|
|
925
|
+
const moduleName = node.childForFieldName?.("module_name")?.text;
|
|
926
|
+
if (moduleName) {
|
|
927
|
+
const isRelative = moduleName.startsWith(".");
|
|
928
|
+
imports.push({ module: moduleName, isRelative });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return imports;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async getOutgoingCalls(
|
|
938
|
+
filePath: string,
|
|
939
|
+
content: string,
|
|
940
|
+
line: number,
|
|
941
|
+
character: number,
|
|
942
|
+
): Promise<
|
|
943
|
+
Array<{ name: string; file: string; line: number; sourceContext?: string }>
|
|
944
|
+
> {
|
|
945
|
+
if (!this.parser || !this.lang) return [];
|
|
946
|
+
const tree = this.parser.parse(content);
|
|
947
|
+
if (!tree) return [];
|
|
948
|
+
|
|
949
|
+
const calls: Array<{
|
|
950
|
+
name: string;
|
|
951
|
+
file: string;
|
|
952
|
+
line: number;
|
|
953
|
+
sourceContext?: string;
|
|
954
|
+
}> = [];
|
|
955
|
+
|
|
956
|
+
// Python calls - using AST walking like analyzer.ts
|
|
957
|
+
if (this.languageName === "python") {
|
|
958
|
+
const defTypes = new Set(["function_definition", "class_definition"]);
|
|
959
|
+
const callNodes = this.collectNodes(tree.rootNode, new Set(["call"]));
|
|
960
|
+
|
|
961
|
+
for (const callNode of callNodes) {
|
|
962
|
+
// Get function name from the call
|
|
963
|
+
const funcNode = callNode.childForFieldName?.("function");
|
|
964
|
+
if (!funcNode) continue;
|
|
965
|
+
|
|
966
|
+
let funcName = funcNode.text;
|
|
967
|
+
const callLine = callNode.startPosition.row + 1;
|
|
968
|
+
|
|
969
|
+
// Handle attribute access (e.g., obj.method -> keep full path for resolution)
|
|
970
|
+
// For the name, extract just the function/method name
|
|
971
|
+
if (funcNode.type === "attribute") {
|
|
972
|
+
const parts = funcName.split(".");
|
|
973
|
+
funcName = parts[parts.length - 1] || funcName;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Find the containing function/class to set sourceContext
|
|
977
|
+
const parentDef = this.findParent(callNode, defTypes);
|
|
978
|
+
const sourceContext = parentDef?.childForFieldName?.("name")?.text;
|
|
979
|
+
|
|
980
|
+
calls.push({
|
|
981
|
+
name: funcName,
|
|
982
|
+
file: filePath,
|
|
983
|
+
line: callLine,
|
|
984
|
+
sourceContext,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return calls;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async getInheritance(
|
|
993
|
+
filePath: string,
|
|
994
|
+
content: string,
|
|
995
|
+
): Promise<Array<{ className: string; parentName: string; line: number }>> {
|
|
996
|
+
if (!this.parser || !this.lang) return [];
|
|
997
|
+
const tree = this.parser.parse(content);
|
|
998
|
+
|
|
999
|
+
const ext = filePath.split(".").pop() || "";
|
|
1000
|
+
const queries = getQueriesForExtension(ext);
|
|
1001
|
+
if (!queries?.inheritance) return [];
|
|
1002
|
+
|
|
1003
|
+
const inheritance: Array<{
|
|
1004
|
+
className: string;
|
|
1005
|
+
parentName: string;
|
|
1006
|
+
line: number;
|
|
1007
|
+
}> = [];
|
|
1008
|
+
|
|
1009
|
+
try {
|
|
1010
|
+
const query = new Query(this.lang, queries.inheritance);
|
|
1011
|
+
const matches = query.matches(tree.rootNode);
|
|
1012
|
+
|
|
1013
|
+
for (const m of matches) {
|
|
1014
|
+
const classNode = m.captures.find(
|
|
1015
|
+
(c: any) => c.name === "class_name",
|
|
1016
|
+
)?.node;
|
|
1017
|
+
const parentNode = m.captures.find(
|
|
1018
|
+
(c: any) => c.name === "parent",
|
|
1019
|
+
)?.node;
|
|
1020
|
+
|
|
1021
|
+
if (classNode && parentNode) {
|
|
1022
|
+
inheritance.push({
|
|
1023
|
+
className: classNode.text,
|
|
1024
|
+
parentName: parentNode.text,
|
|
1025
|
+
line: classNode.startPosition.row + 1,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
} catch {
|
|
1030
|
+
// Query failed
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return inheritance;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* ----------------------------------------------------------------------
|
|
1039
|
+
* TIER 3: Regex Provider (Fallback)
|
|
1040
|
+
* ----------------------------------------------------------------------
|
|
1041
|
+
*/
|
|
1042
|
+
class RegexProvider implements LanguageProvider {
|
|
1043
|
+
private config: Record<
|
|
1044
|
+
string,
|
|
1045
|
+
{
|
|
1046
|
+
defPattern: RegExp;
|
|
1047
|
+
kindMap: (match: RegExpMatchArray) => string;
|
|
1048
|
+
nameIdx: number;
|
|
1049
|
+
extensions: string[];
|
|
1050
|
+
}
|
|
1051
|
+
> = {
|
|
1052
|
+
python: {
|
|
1053
|
+
defPattern: /^\s*(?:async\s+)?(def|class)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm,
|
|
1054
|
+
kindMap: (m) => (m[1] === "class" ? "Class" : "Function"),
|
|
1055
|
+
nameIdx: 2,
|
|
1056
|
+
extensions: ["py"],
|
|
1057
|
+
},
|
|
1058
|
+
rust: {
|
|
1059
|
+
defPattern:
|
|
1060
|
+
/^\s*(?:pub\s+)?(fn|struct|enum|trait|impl)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm,
|
|
1061
|
+
kindMap: (m) => {
|
|
1062
|
+
const type = m[1] || "";
|
|
1063
|
+
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
1064
|
+
},
|
|
1065
|
+
nameIdx: 2,
|
|
1066
|
+
extensions: ["rs"],
|
|
1067
|
+
},
|
|
1068
|
+
go: {
|
|
1069
|
+
defPattern: /^\s*func\s+(?:.*?\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\(/gm,
|
|
1070
|
+
kindMap: () => "Function",
|
|
1071
|
+
nameIdx: 1,
|
|
1072
|
+
extensions: ["go"],
|
|
1073
|
+
},
|
|
1074
|
+
default: {
|
|
1075
|
+
defPattern: /^\s*(function|class|interface)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm,
|
|
1076
|
+
kindMap: (m) => m[1] || "Unknown",
|
|
1077
|
+
nameIdx: 2,
|
|
1078
|
+
extensions: [],
|
|
1079
|
+
},
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
isSupported(ext: string): boolean {
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private getConfig(ext: string) {
|
|
1087
|
+
for (const key in this.config) {
|
|
1088
|
+
if (this.config[key]!.extensions.includes(ext)) {
|
|
1089
|
+
return this.config[key]!;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return this.config.default!;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async getDiagnostics() {
|
|
1096
|
+
return { errors: [], warnings: [] };
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async findDefinition(
|
|
1100
|
+
filePath: string,
|
|
1101
|
+
content: string,
|
|
1102
|
+
line: number,
|
|
1103
|
+
character: number,
|
|
1104
|
+
) {
|
|
1105
|
+
const lines = content.split("\n");
|
|
1106
|
+
const docLine = lines[line - 1];
|
|
1107
|
+
if (!docLine) return null;
|
|
1108
|
+
|
|
1109
|
+
// Extract word at cursor
|
|
1110
|
+
const match = docLine.slice(0, character).match(/[a-zA-Z0-9_]+$/);
|
|
1111
|
+
const suffix = docLine.slice(character).match(/^[a-zA-Z0-9_]+/);
|
|
1112
|
+
const word = (match ? match[0] : "") + (suffix ? suffix[0] : "");
|
|
1113
|
+
if (!word) return null;
|
|
1114
|
+
|
|
1115
|
+
const ext = filePath.split(".").pop() || "";
|
|
1116
|
+
const cfg = this.getConfig(ext);
|
|
1117
|
+
|
|
1118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1119
|
+
const lineContent = lines[i]!;
|
|
1120
|
+
if (lineContent.includes(word)) {
|
|
1121
|
+
const regex = new RegExp(cfg.defPattern.source, "gm");
|
|
1122
|
+
let match;
|
|
1123
|
+
while ((match = regex.exec(lineContent)) !== null) {
|
|
1124
|
+
if (match[cfg.nameIdx] === word) {
|
|
1125
|
+
const preview = lines.slice(Math.max(0, i - 1), i + 2).join("\n");
|
|
1126
|
+
return { file: filePath, line: i + 1, preview };
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async getSymbolInfo(
|
|
1136
|
+
filePath: string,
|
|
1137
|
+
content: string,
|
|
1138
|
+
line: number,
|
|
1139
|
+
character: number,
|
|
1140
|
+
) {
|
|
1141
|
+
const def = await this.findDefinition(filePath, content, line, character);
|
|
1142
|
+
if (def) return `Defined at line ${def.line}`;
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async findImplementation() {
|
|
1147
|
+
return [];
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
async findReferences(
|
|
1151
|
+
filePath: string,
|
|
1152
|
+
content: string,
|
|
1153
|
+
line: number,
|
|
1154
|
+
character: number,
|
|
1155
|
+
) {
|
|
1156
|
+
const lines = content.split("\n");
|
|
1157
|
+
const docLine = lines[line - 1];
|
|
1158
|
+
if (!docLine) return [];
|
|
1159
|
+
|
|
1160
|
+
const match = docLine.slice(0, character).match(/[a-zA-Z0-9_]+$/);
|
|
1161
|
+
const suffix = docLine.slice(character).match(/^[a-zA-Z0-9_]+/);
|
|
1162
|
+
const word = (match ? match[0] : "") + (suffix ? suffix[0] : "");
|
|
1163
|
+
if (!word) return [];
|
|
1164
|
+
|
|
1165
|
+
const refs: Array<{ file: string; line: number }> = [];
|
|
1166
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1167
|
+
if (lines[i]!.includes(word)) {
|
|
1168
|
+
refs.push({ file: filePath, line: i + 1 });
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return refs;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async getDocumentSymbols(filePath: string, content: string) {
|
|
1175
|
+
const ext = filePath.split(".").pop() || "";
|
|
1176
|
+
const cfg = this.getConfig(ext);
|
|
1177
|
+
|
|
1178
|
+
const symbols: any[] = [];
|
|
1179
|
+
const lines = content.split("\n");
|
|
1180
|
+
|
|
1181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1182
|
+
const regex = new RegExp(cfg.defPattern.source, "g");
|
|
1183
|
+
let match;
|
|
1184
|
+
while ((match = regex.exec(lines[i]!)) !== null) {
|
|
1185
|
+
// Regex provider can't reliably determine end lines without parsing blocks
|
|
1186
|
+
// So we return flat list for now, or could try indentation heuristics
|
|
1187
|
+
symbols.push({
|
|
1188
|
+
name: match[cfg.nameIdx],
|
|
1189
|
+
kind: cfg.kindMap(match),
|
|
1190
|
+
line: i + 1,
|
|
1191
|
+
// Fallback: assume single line or try to guess?
|
|
1192
|
+
// For now, regex provider remains flat or we can try to guess scope by indentation
|
|
1193
|
+
endLine: i + 1, // Basic assumption
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
return symbols;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* ----------------------------------------------------------------------
|
|
1204
|
+
* CLIENT: Universal LSP Client
|
|
1205
|
+
* ----------------------------------------------------------------------
|
|
1206
|
+
*/
|
|
1207
|
+
export class UniversalLSPClient {
|
|
1208
|
+
private tsProvider: TypeScriptProvider;
|
|
1209
|
+
private pyProvider: TreeSitterProvider;
|
|
1210
|
+
private regexProvider: RegexProvider;
|
|
1211
|
+
|
|
1212
|
+
constructor(private rootPath: string = process.cwd()) {
|
|
1213
|
+
this.tsProvider = new TypeScriptProvider(rootPath);
|
|
1214
|
+
this.regexProvider = new RegexProvider();
|
|
1215
|
+
|
|
1216
|
+
// Use import.meta.dir for Bun to get module-relative path
|
|
1217
|
+
// Fall back to rootPath for Node.js compatibility
|
|
1218
|
+
// Find the project root robustly for both development and bundled production
|
|
1219
|
+
const currentDir = import.meta.dir;
|
|
1220
|
+
let moduleDir = currentDir;
|
|
1221
|
+
|
|
1222
|
+
// If in dist/ or src/utils/, go up until we find the root
|
|
1223
|
+
if (currentDir.endsWith("dist") || currentDir.endsWith("utils")) {
|
|
1224
|
+
moduleDir = path.resolve(currentDir, "..");
|
|
1225
|
+
if (currentDir.endsWith("utils")) {
|
|
1226
|
+
moduleDir = path.resolve(moduleDir, "..");
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const pythonWasm = path.resolve(
|
|
1231
|
+
moduleDir,
|
|
1232
|
+
"src/parsers/tree-sitter-python.wasm",
|
|
1233
|
+
);
|
|
1234
|
+
this.pyProvider = new TreeSitterProvider("python", pythonWasm);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
private async getProvider(filePath: string): Promise<LanguageProvider> {
|
|
1238
|
+
const ext = filePath.split(".").pop() || "";
|
|
1239
|
+
|
|
1240
|
+
if (this.tsProvider.isSupported(ext)) {
|
|
1241
|
+
return this.tsProvider;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// For Python, ensure tree-sitter is ready before checking
|
|
1245
|
+
if (ext === "py") {
|
|
1246
|
+
const ready = await this.pyProvider.ensureReady();
|
|
1247
|
+
if (ready) {
|
|
1248
|
+
return this.pyProvider;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
return this.regexProvider;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
async getDiagnostics(filePath: string, content: string) {
|
|
1256
|
+
const provider = await this.getProvider(filePath);
|
|
1257
|
+
return provider.getDiagnostics(filePath, content);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
async findDefinition(
|
|
1261
|
+
filePath: string,
|
|
1262
|
+
content: string,
|
|
1263
|
+
line: number,
|
|
1264
|
+
character: number,
|
|
1265
|
+
) {
|
|
1266
|
+
const provider = await this.getProvider(filePath);
|
|
1267
|
+
return provider.findDefinition(filePath, content, line, character);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
async findReferences(
|
|
1271
|
+
filePath: string,
|
|
1272
|
+
content: string,
|
|
1273
|
+
line: number,
|
|
1274
|
+
character: number,
|
|
1275
|
+
) {
|
|
1276
|
+
const provider = await this.getProvider(filePath);
|
|
1277
|
+
return provider.findReferences(filePath, content, line, character);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async getDocumentSymbols(filePath: string, content: string) {
|
|
1281
|
+
const provider = await this.getProvider(filePath);
|
|
1282
|
+
return provider.getDocumentSymbols(filePath, content);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
async getSymbolInfo(
|
|
1286
|
+
filePath: string,
|
|
1287
|
+
content: string,
|
|
1288
|
+
line: number,
|
|
1289
|
+
character: number,
|
|
1290
|
+
) {
|
|
1291
|
+
const provider = await this.getProvider(filePath);
|
|
1292
|
+
return provider.getSymbolInfo(filePath, content, line, character);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
async findImplementation(
|
|
1296
|
+
filePath: string,
|
|
1297
|
+
content: string,
|
|
1298
|
+
line: number,
|
|
1299
|
+
character: number,
|
|
1300
|
+
) {
|
|
1301
|
+
const provider = await this.getProvider(filePath);
|
|
1302
|
+
return provider.findImplementation(filePath, content, line, character);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async prepareCallHierarchy(
|
|
1306
|
+
filePath: string,
|
|
1307
|
+
content: string,
|
|
1308
|
+
line: number,
|
|
1309
|
+
character: number,
|
|
1310
|
+
) {
|
|
1311
|
+
const provider = await this.getProvider(filePath);
|
|
1312
|
+
return provider.prepareCallHierarchy
|
|
1313
|
+
? provider.prepareCallHierarchy(filePath, content, line, character)
|
|
1314
|
+
: [];
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async getIncomingCalls(
|
|
1318
|
+
filePath: string,
|
|
1319
|
+
content: string,
|
|
1320
|
+
line: number,
|
|
1321
|
+
character: number,
|
|
1322
|
+
) {
|
|
1323
|
+
const provider = await this.getProvider(filePath);
|
|
1324
|
+
return provider.getIncomingCalls
|
|
1325
|
+
? provider.getIncomingCalls(filePath, content, line, character)
|
|
1326
|
+
: [];
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
async getOutgoingCalls(
|
|
1330
|
+
filePath: string,
|
|
1331
|
+
content: string,
|
|
1332
|
+
line: number,
|
|
1333
|
+
character: number,
|
|
1334
|
+
) {
|
|
1335
|
+
const provider = await this.getProvider(filePath);
|
|
1336
|
+
return provider.getOutgoingCalls
|
|
1337
|
+
? provider.getOutgoingCalls(filePath, content, line, character)
|
|
1338
|
+
: [];
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
async searchWorkspaceSymbols(query: string, filePath: string) {
|
|
1342
|
+
const provider = await this.getProvider(filePath);
|
|
1343
|
+
return provider.searchWorkspaceSymbols
|
|
1344
|
+
? provider.searchWorkspaceSymbols(query)
|
|
1345
|
+
: [];
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async getImports(filePath: string, content: string) {
|
|
1349
|
+
const provider = await this.getProvider(filePath);
|
|
1350
|
+
return provider.getImports ? provider.getImports(filePath, content) : [];
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
async getInheritance(filePath: string, content: string) {
|
|
1354
|
+
const provider = await this.getProvider(filePath);
|
|
1355
|
+
return provider.getInheritance
|
|
1356
|
+
? provider.getInheritance(filePath, content)
|
|
1357
|
+
: [];
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
async getDocumentSymbolsWithDetails(
|
|
1361
|
+
filePath: string,
|
|
1362
|
+
content: string,
|
|
1363
|
+
): Promise<DetailedSymbol[]> {
|
|
1364
|
+
const provider = await this.getProvider(filePath);
|
|
1365
|
+
return provider.getDocumentSymbolsWithDetails
|
|
1366
|
+
? provider.getDocumentSymbolsWithDetails(filePath, content)
|
|
1367
|
+
: this.fallbackToBasicSymbols(filePath, content);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private async fallbackToBasicSymbols(
|
|
1371
|
+
filePath: string,
|
|
1372
|
+
content: string,
|
|
1373
|
+
): Promise<DetailedSymbol[]> {
|
|
1374
|
+
// Convert basic symbols to detailed format when provider doesn't support it
|
|
1375
|
+
const provider = await this.getProvider(filePath);
|
|
1376
|
+
const basic = await provider.getDocumentSymbols(filePath, content);
|
|
1377
|
+
return basic.map((s) => ({
|
|
1378
|
+
name: s.name,
|
|
1379
|
+
kind: s.kind,
|
|
1380
|
+
line: s.line,
|
|
1381
|
+
children: s.children?.map((c: any) => ({
|
|
1382
|
+
name: c.name,
|
|
1383
|
+
kind: c.kind,
|
|
1384
|
+
line: c.line,
|
|
1385
|
+
})),
|
|
1386
|
+
}));
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
isSupported(filePath: string): boolean {
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
getSupportedExtensions(): string[] {
|
|
1394
|
+
return ["ts", "js", "py", "rs", "go", "*"];
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
shutdown() {
|
|
1398
|
+
// No-op
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
let clientInstance: UniversalLSPClient | null = null;
|
|
1403
|
+
|
|
1404
|
+
export function getLSPClient(rootPath?: string): UniversalLSPClient {
|
|
1405
|
+
if (!clientInstance) {
|
|
1406
|
+
clientInstance = new UniversalLSPClient(rootPath);
|
|
1407
|
+
}
|
|
1408
|
+
return clientInstance;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
export function resetLSPClient(): void {
|
|
1412
|
+
clientInstance = null;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
export default UniversalLSPClient;
|