diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0125ac46..20fe049f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -97,7 +97,7 @@ If you ran `npm run watch`, the build is done when you see something like this:
-1. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `/.scripts/code.bat` (Windows). This should open up the built IDE!
+1. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `./scripts/code.bat` (Windows). This should open up the built IDE!
You can always press Ctrl+Shift+P and run "Reload Window" inside the new window to see changes without re-building.
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json
index 15226ca5..283fcb8c 100644
--- a/extensions/void/package-lock.json
+++ b/extensions/void/package-lock.json
@@ -7,6 +7,12 @@
"": {
"name": "void",
"version": "0.0.1",
+ "dependencies": {
+ "lru-cache": "^11.0.2",
+ "tree-sitter": "^0.21.1",
+ "tree-sitter-javascript": "^0.23.1",
+ "tree-sitter-python": "^0.23.4"
+ },
"devDependencies": {
"@anthropic-ai/sdk": "^0.29.2",
"@eslint/js": "^9.9.1",
@@ -35,8 +41,8 @@
"eslint-plugin-react": "^7.35.1",
"eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.9.0",
- "groq-sdk": "^0.8.0",
"lodash": "^4.17.21",
+ "groq-sdk": "^0.8.0",
"marked": "^14.1.0",
"ollama": "^0.5.9",
"openai": "^4.68.4",
@@ -4977,12 +4983,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
- },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -5047,11 +5047,12 @@
}
},
"node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
+ "version": "11.0.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz",
+ "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
+ "engines": {
+ "node": "20 || >=22"
+ }
},
"node_modules/make-dir": {
"version": "4.0.0",
@@ -5970,7 +5971,6 @@
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
"dev": true,
- "license": "MIT",
"peer": true
},
"node_modules/ms": {
@@ -6018,6 +6018,14 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-addon-api": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.2.tgz",
+ "integrity": "sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -6059,6 +6067,16 @@
}
}
},
+ "node_modules/node-gyp-build": {
+ "version": "4.8.3",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
+ "integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@@ -6560,6 +6578,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
@@ -7400,16 +7424,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/rimraf/node_modules/lru-cache": {
- "version": "11.0.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz",
- "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "20 || >=22"
- }
- },
"node_modules/rimraf/node_modules/minimatch": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
@@ -8208,6 +8222,52 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tree-sitter": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
+ "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "node-addon-api": "^8.0.0",
+ "node-gyp-build": "^4.8.0"
+ }
+ },
+ "node_modules/tree-sitter-javascript": {
+ "version": "0.23.1",
+ "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.23.1.tgz",
+ "integrity": "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "node-addon-api": "^8.2.2",
+ "node-gyp-build": "^4.8.2"
+ },
+ "peerDependencies": {
+ "tree-sitter": "^0.21.1"
+ },
+ "peerDependenciesMeta": {
+ "tree-sitter": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tree-sitter-python": {
+ "version": "0.23.4",
+ "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.23.4.tgz",
+ "integrity": "sha512-MbmUAl7y5UCUWqHscHke7DdRDwQnVNMNKQYQc4Gq2p09j+fgPxaU8JVsuOI/0HD3BSEEe5k9j3xmdtIWbDtDgw==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "node-addon-api": "^8.2.1",
+ "node-gyp-build": "^4.8.2"
+ },
+ "peerDependencies": {
+ "tree-sitter": "^0.21.1"
+ },
+ "peerDependenciesMeta": {
+ "tree-sitter": {
+ "optional": true
+ }
+ }
+ },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
diff --git a/extensions/void/package.json b/extensions/void/package.json
index 4584f9eb..2cc8e3a7 100644
--- a/extensions/void/package.json
+++ b/extensions/void/package.json
@@ -21,6 +21,10 @@
"properties": {}
},
"commands": [
+ {
+ "command": "typeInspector.inspect",
+ "title": "Inspect Types of All Variables"
+ },
{
"command": "void.ctrl+l",
"title": "Show Sidebar"
@@ -156,5 +160,11 @@
"typescript": "5.5.4",
"typescript-eslint": "^8.3.0",
"uuid": "^10.0.0"
+ },
+ "dependencies": {
+ "lru-cache": "^11.0.2",
+ "tree-sitter": "^0.21.1",
+ "tree-sitter-javascript": "^0.23.1",
+ "tree-sitter-python": "^0.23.4"
}
}
diff --git a/extensions/void/src/common/LangaugeServer/createJsProgramGraph.ts b/extensions/void/src/common/LangaugeServer/createJsProgramGraph.ts
new file mode 100644
index 00000000..4fb9bcaf
--- /dev/null
+++ b/extensions/void/src/common/LangaugeServer/createJsProgramGraph.ts
@@ -0,0 +1,333 @@
+import * as vscode from 'vscode';
+import Parser from 'tree-sitter';
+import JavaScript from 'tree-sitter-javascript';
+
+interface Definition {
+ file: string;
+ node: Parser.SyntaxNode;
+}
+
+interface DefnUse {
+ parent: Parser.SyntaxNode;
+ file: string;
+}
+
+interface ImportInfo {
+ source: string;
+ imported: string;
+}
+
+class ProjectAnalyzer {
+ private parser: Parser;
+ private graph: Map>;
+ private visited: Set;
+ private parsedFiles: Map;
+ private imports: Map>;
+ private definitions: Map;
+ private fileStack: Set;
+
+ constructor() {
+ this.parser = new Parser();
+ this.parser.setLanguage(JavaScript);
+ this.graph = new Map();
+ this.visited = new Set();
+ this.parsedFiles = new Map();
+ this.imports = new Map();
+ this.definitions = new Map();
+ this.fileStack = new Set();
+ }
+
+ async parseFile(filePath: string): Promise {
+ if (this.parsedFiles.has(filePath)) {
+ return this.parsedFiles.get(filePath)!;
+ }
+
+ if (this.fileStack.has(filePath)) {
+ return null; // Circular import
+ }
+
+ this.fileStack.add(filePath);
+
+ try {
+ const uri = vscode.Uri.file(filePath);
+ const document = await vscode.workspace.openTextDocument(uri);
+ const code = document.getText();
+ const tree = this.parser.parse(code);
+
+ this.parsedFiles.set(filePath, tree);
+ this.collectImports(filePath, tree);
+ this.collectDefinitions(filePath, tree);
+
+ return tree;
+ } catch (error) {
+ console.error(`Error parsing ${filePath}:`, error);
+ return null;
+ } finally {
+ this.fileStack.delete(filePath);
+ }
+ }
+
+ private collectImports(filePath: string, tree: Parser.Tree): void {
+ const fileImports = new Map();
+
+ const visit = (node: Parser.SyntaxNode): void => {
+ if (node.type === 'import_declaration') {
+ const source = node.childForFieldName('source')?.text.slice(1, -1) ?? '';
+ const specifiers = node.childForFieldName('specifiers');
+
+ specifiers?.children.forEach(spec => {
+ if (spec.type === 'import_specifier') {
+ const local = spec.childForFieldName('local')?.text ?? '';
+ const imported = spec.childForFieldName('imported')?.text ?? '';
+ fileImports.set(local, { source, imported });
+ }
+ });
+ }
+ node.children.forEach(visit);
+ };
+
+ visit(tree.rootNode);
+ this.imports.set(filePath, fileImports);
+ }
+
+ private collectDefinitions(filePath: string, tree: Parser.Tree): void {
+ const visit = (node: Parser.SyntaxNode): void => {
+ if (node.type === 'function_declaration') {
+ const name = node.childForFieldName('name')?.text ?? '';
+ this.definitions.set(name, { file: filePath, node });
+ }
+ else if (node.type === 'variable_declarator') {
+ const name = node.childForFieldName('name')?.text;
+ const value = node.childForFieldName('value');
+ if (name && (value?.type === 'arrow_function' || value?.type === 'function')) {
+ this.definitions.set(name, { file: filePath, node: value });
+ }
+ }
+ node.children.forEach(visit);
+ };
+
+ visit(tree.rootNode);
+ }
+
+ private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise {
+ const hover = await vscode.commands.executeCommand(
+ 'vscode.executeHoverProvider',
+ uri,
+ position
+ );
+
+ if (hover?.[0]?.contents.length) {
+ for (const content of hover[0].contents) {
+ let hoverText = typeof content === 'string' ?
+ content :
+ ('value' in content ? content.value : '');
+
+ // Remove typescript backticks if present
+ hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, '');
+ console.log('Processing hover text:', hoverText);
+
+ // Extract the type information - look for the type after the colon
+ const typeMatches = [
+ /:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]"
+ /var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type"
+ /\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type"
+ /\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method"
+ ];
+
+ for (const pattern of typeMatches) {
+ const match = pattern.exec(hoverText);
+ if (match) {
+ let type = match[1];
+ // Handle array types
+ if (hoverText.includes('[]')) {
+ return 'Array';
+ }
+ // Extract base type from generics
+ if (type.includes('<')) {
+ type = type.split('<')[0];
+ }
+ return type;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise> {
+ const calls = new Set();
+ const fileImports = this.imports.get(currentFile) ?? new Map();
+ const uri = vscode.Uri.file(currentFile);
+
+ const visit = async (node: Parser.SyntaxNode): Promise => {
+ if (node.type === 'call_expression') {
+ const callee = node.childForFieldName('function');
+ if (callee?.type === 'identifier') {
+ const name = callee.text;
+ const importInfo = fileImports.get(name);
+ if (importInfo) {
+ calls.add(`${importInfo.source}:${importInfo.imported}`);
+ } else {
+ calls.add(name);
+ }
+ }
+ else if (callee?.type === 'member_expression') {
+ const method = callee.childForFieldName('property')?.text;
+ const object = callee.childForFieldName('object');
+
+ if (method && object) {
+ const position = new vscode.Position(
+ object.startPosition.row,
+ object.startPosition.column
+ );
+
+ const type = await this.getTypeFromPosition(uri, position);
+ if (type) {
+ calls.add(`${type}.${method}`);
+ } else {
+ calls.add(`method:${method}`);
+ }
+ }
+ }
+ }
+
+ for (const child of node.children) {
+ await visit(child);
+ }
+ };
+
+ await visit(defnNode);
+ return calls;
+ }
+
+ private gotoDefn(name: string): Definition | null {
+ if (name.includes(':')) {
+ const [file, funcName] = name.split(':');
+ const def = this.definitions.get(funcName);
+ return def ?? null;
+ }
+
+ return this.definitions.get(name) ?? null;
+ }
+
+ private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] {
+ const uses: DefnUse[] = [];
+
+ let fnName: string | undefined;
+ if (defnNode.type === 'function_declaration') {
+ fnName = defnNode.childForFieldName('name')?.text;
+ } else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') {
+ const parent = defnNode.parent;
+ if (parent?.type === 'variable_declarator') {
+ fnName = parent.childForFieldName('name')?.text;
+ }
+ }
+
+ if (!fnName) return uses;
+
+ for (const [file, tree] of this.parsedFiles) {
+ const visit = (node: Parser.SyntaxNode): void => {
+ if (node.type === 'call_expression') {
+ const callee = node.childForFieldName('function');
+ if (callee?.type === 'identifier' && callee.text === fnName) {
+ let current: Parser.SyntaxNode | null = node;
+ while (current) {
+ if (current.type === 'function_declaration' ||
+ current.type === 'arrow_function' ||
+ current.type === 'function') {
+ uses.push({ parent: current, file });
+ break;
+ }
+ current = current.parent;
+ }
+ }
+ }
+ node.children.forEach(visit);
+ };
+
+ visit(tree.rootNode);
+ }
+
+ return uses;
+ }
+
+ private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise {
+ let defnName: string | undefined;
+ if (defn.type === 'function_declaration') {
+ defnName = defn.childForFieldName('name')?.text;
+ } else if (defn.type === 'arrow_function' || defn.type === 'function') {
+ const parent = defn.parent;
+ if (parent?.type === 'variable_declarator') {
+ defnName = parent.childForFieldName('name')?.text;
+ }
+ }
+
+ if (!defnName) return;
+
+ const fullName = `${currentFile}:${defnName}`;
+ if (this.visited.has(fullName)) return;
+
+ const calls = await this.getCallsInDefn(defn, currentFile);
+ this.graph.set(fullName, calls);
+ this.visited.add(fullName);
+
+ const callDefns = Array.from(calls).map(call => this.gotoDefn(call));
+ for (const callDefn of callDefns) {
+ if (callDefn) {
+ await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file);
+ }
+ }
+
+ const defnUses = this.getUses(defn, currentFile);
+ for (const defnUse of defnUses) {
+ await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file);
+ }
+ }
+
+ async analyze(entryFile: string): Promise