diff --git a/.idx/dev.nix b/.idx/dev.nix
new file mode 100644
index 00000000..83eb293f
--- /dev/null
+++ b/.idx/dev.nix
@@ -0,0 +1,48 @@
+# Created for Void
+# To learn more about how to use Nix to configure your environment
+# see: https://developers.google.com/idx/guides/customize-idx-env
+{pkgs}: {
+ # Which nixpkgs channel to use.
+ channel = "stable-23.11"; # or "unstable"
+ # Use https://search.nixos.org/packages to find packages
+ packages = [
+ pkgs.nodejs_20
+ pkgs.yarn
+ pkgs.nodePackages.pnpm
+ pkgs.bun
+ pkgs.gh
+ ];
+ # Sets environment variables in the workspace
+ env = {};
+ idx = {
+ # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
+ extensions = [
+ # "vscodevim.vim"
+ ];
+ workspace = {
+ # Runs when a workspace is first created with this `dev.nix` file
+ onCreate = {
+ npm-install = "npm ci --no-audit --prefer-offline --no-progress --timing";
+ # Open editors for the following files by default, if they exist:
+ default.openFiles = [
+ # Cover all the variations of language, src-dir, router (app/pages)
+ "pages/index.tsx" "pages/index.jsx"
+ "src/pages/index.tsx" "src/pages/index.jsx"
+ "app/page.tsx" "app/page.jsx"
+ "src/app/page.tsx" "src/app/page.jsx"
+ ];
+ };
+ # To run something each time the workspace is (re)started, use the `onStart` hook
+ };
+ # Enable previews and customize configuration
+ previews = {
+ enable = true;
+ previews = {
+ web = {
+ command = ["npm" "run" "dev" "--" "--port" "$PORT" "--hostname" "0.0.0.0"];
+ manager = "web";
+ };
+ };
+ };
+ };
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4b27e119..f06d13d1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,37 +9,9 @@ There are a few ways to contribute:
- Submit Issues/Docs/Bugs ([Issues](https://github.com/voideditor/void/issues))
-## 1. Building the Extension
+## 2. Building the IDE
-Here's how you can start contributing to the Void extension. This is where you should get started if you're new.
-
-1. Clone the repository:
-
-```
-git clone https://github.com/voideditor/void
-```
-
-2. Open the folder `/extensions/void` in VSCode (open it in a new workspace, _don't_ just cd into it).
-
-3. Install dependencies:
-
-```
-npm install
-```
-
-1. Compile the React files by running `npm run build`. This build command converts all the Tailwind/React entrypoint files into raw .css and .js files in `dist/`.
-
-```
-npm run build
-```
-
-5. Run the extension in a new window by pressing F5.
-
-This will start a new instance of VSCode with the extension enabled. If this doesn't work, you can press Ctrl+Shift+P, select "Debug: Start Debugging", and select "VSCode Extension Development".
-
-## 2. Building the full IDE
-
-If you want to work on the full IDE, please follow the steps below. If you have any questions/issues, you can refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. Also feel free to submit an issue or get in touch with us with any build errors.
+Please follow the steps below to build the IDE. If you have any questions/issues, you can refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. Also feel free to [submit an issue](https://github.com/voideditor/void/issues/new) or get in touch with us with any build errors.
@@ -70,7 +42,7 @@ We haven't created prerequisite steps for building on Linux yet, but you can fol
### Build instructions
-Before building Void, please follow the prerequisite steps above for your operating system. Also, make sure you've already built and compiled the Void extension (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`).
+Before building Void, please follow the prerequisite steps above for your operating system. Also, make sure you've already built and compiled the Void React components by running `cd ./` (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`).
To build Void, first open `void/` in VSCode. Then:
@@ -80,7 +52,9 @@ To build Void, first open `void/` in VSCode. Then:
npm install
```
-2. Press Ctrl+Shift+B, or if you prefer using the terminal run `npm run watch`.
+2. Build Void's React components by running `cd ./src/vs/workbench/contrib/void/browser/react/`, and executing the build script with `node ./build.js`. You might need to run `npm i -g tsup` if this doesn't work.
+
+3. Press Ctrl+Shift+B, or if you prefer using the terminal run `npm run watch`.
This can take ~5 min.
@@ -97,11 +71,10 @@ 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!
+4. 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!
-
+Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page.
### Common Fixes
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