├── fixtures ├── pixi-workspace │ ├── .gitignore │ ├── main.mojo │ ├── .gitattributes │ └── pixi.toml └── dangling-file │ └── dangling_file.mojo ├── icon.png ├── bin ├── fastlist-0.3.0-x64.exe └── fastlist-0.3.0-x86.exe ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── .vscodeignore ├── esbuild.mjs ├── lsp-proxy ├── tsconfig.json ├── package.json ├── src │ ├── proxy.ts │ ├── DisposableContext.ts │ ├── types.ts │ ├── streams.ts │ ├── MojoLSPServer.ts │ └── MojoLSPProxy.ts └── package-lock.json ├── eslint.config.mjs ├── .vscode-test.mjs ├── tsconfig.json ├── .vscode ├── tasks.json └── launch.json ├── .github ├── actions │ └── setup │ │ └── action.yaml ├── workflows │ ├── lint.yaml │ ├── build.yaml │ ├── test.yaml │ ├── cla.yaml │ └── deploy.yaml ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── extension ├── types.ts ├── debug │ ├── constants.ts │ ├── types.ts │ ├── attachQuickPick.ts │ └── inlineVariables.ts ├── pyenv.test.pixi.ts ├── utils │ ├── disposableContext.ts │ ├── checkNsight.ts │ ├── vscodeVariables.ts │ ├── config.ts │ ├── files.ts │ └── configWatcher.ts ├── lsp │ ├── lsp.test.pixi.ts │ ├── recorder.ts │ └── lsp.ts ├── formatter.ts ├── telemetry.ts ├── test │ └── reporter.ts ├── logging.test.default.ts ├── logging.ts ├── decorations.ts ├── extension.ts ├── external │ └── psList.ts ├── server │ └── RpcServer.ts ├── commands │ └── run.ts └── pyenv.ts ├── .pre-commit-config.yaml ├── syntaxes └── markdown.syntax.json ├── language-configuration.json ├── utils └── license.py ├── README.md └── LICENSE /fixtures/pixi-workspace/.gitignore: -------------------------------------------------------------------------------- 1 | # pixi environments 2 | .pixi 3 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modular/vscode-mojo/HEAD/icon.png -------------------------------------------------------------------------------- /fixtures/pixi-workspace/main.mojo: -------------------------------------------------------------------------------- 1 | fn main(): 2 | print("hello world") 3 | -------------------------------------------------------------------------------- /bin/fastlist-0.3.0-x64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modular/vscode-mojo/HEAD/bin/fastlist-0.3.0-x64.exe -------------------------------------------------------------------------------- /bin/fastlist-0.3.0-x86.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modular/vscode-mojo/HEAD/bin/fastlist-0.3.0-x86.exe -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | .vscode-test/ 4 | mojo-lsp-recording.jsonl 5 | */tsconfig.tsbuildinfo 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "plugins": ["prettier-plugin-curly"] 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/pixi-workspace/.gitattributes: -------------------------------------------------------------------------------- 1 | # SCM syntax highlighting & preventing 3-way merges 2 | pixi.lock merge=binary linguist-language=YAML linguist-generated=true 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The changelogs can be found in the following links: 4 | 5 | - [Stable release changelog](https://docs.modular.com/mojo/changelog) 6 | - [Nightly release changelog](https://github.com/modular/mojo/blob/nightly/docs/changelog.md) 7 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .gitignore 4 | .github/** 5 | fixtures/ 6 | lsp-proxy/** 7 | !lsp-proxy/out/** 8 | extension/** 9 | eslint.config.mjs 10 | CONTRIBUTING.md 11 | .prettierrc 12 | .vscode-test.mjs 13 | **/*.ts 14 | node_modules/** 15 | tsconfig.json 16 | esbuild.js 17 | -------------------------------------------------------------------------------- /fixtures/pixi-workspace/pixi.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | authors = ["Lily Brown "] 3 | channels = ["https://conda.modular.com/max-nightly", "conda-forge"] 4 | name = "pixi-workspace" 5 | platforms = ["linux-64"] 6 | version = "0.1.0" 7 | 8 | [tasks] 9 | 10 | [dependencies] 11 | modular = "25.5.0.dev2025071605" 12 | -------------------------------------------------------------------------------- /fixtures/dangling-file/dangling_file.mojo: -------------------------------------------------------------------------------- 1 | # ===----------------------------------------------------------------------=== # 2 | # 3 | # This file is Modular Inc proprietary. 4 | # 5 | # ===----------------------------------------------------------------------=== # 6 | 7 | 8 | fn main(): 9 | var some_string = String("let_string") 10 | print(some_string) 11 | -------------------------------------------------------------------------------- /esbuild.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | 3 | await esbuild.build({ 4 | entryPoints: ['extension/extension.ts'], 5 | bundle: true, 6 | outfile: 'out/extension.js', 7 | platform: 'node', 8 | external: ['vscode'], 9 | }); 10 | 11 | await esbuild.build({ 12 | entryPoints: ['lsp-proxy/src/proxy.ts'], 13 | bundle: true, 14 | outfile: 'out/proxy.js', 15 | platform: 'node', 16 | external: ['vscode'], 17 | }); 18 | -------------------------------------------------------------------------------- /lsp-proxy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "out", 5 | "rootDir": "src", 6 | "sourceMap": true, 7 | "composite": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "target": "es6", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true 13 | }, 14 | "include": ["src"], 15 | "exclude": ["node_modules", ".vscode-test"] 16 | } 17 | -------------------------------------------------------------------------------- /lsp-proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mojo-lsp-proxy", 3 | "description": "Proxy server for the Mojo Language Server.", 4 | "version": "v2024.5.1003", 5 | "author": "modular-mojotools", 6 | "engines": { 7 | "node": "*" 8 | }, 9 | "dependencies": { 10 | "rxjs": "7.8.1", 11 | "vscode-languageserver": "9.0.1", 12 | "vscode-languageserver-protocol": "3.17.5", 13 | "vscode-languageserver-textdocument": "1.0.4" 14 | }, 15 | "scripts": { 16 | "vscode:prepublish": "tsc -p ./", 17 | "watch": "tsc -watch -p ./" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | 6 | export default tseslint.config( 7 | { 8 | ignores: ['out/**', 'lsp-proxy/out/**'], 9 | }, 10 | eslint.configs.recommended, 11 | tseslint.configs.recommended, 12 | { 13 | rules: { 14 | // TODO: We shouldn't be doing this. 15 | '@typescript-eslint/no-explicit-any': 'off', 16 | 17 | // We deliberately specify unused function parameters prefixed with _ 18 | '@typescript-eslint/no-unused-vars': 'off', 19 | }, 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | const baseConfig = { 4 | platform: 'desktop', 5 | version: '1.92.2', 6 | mocha: { 7 | timeout: 5 * 60 * 1000, 8 | reporter: 'out/test/reporter.js', 9 | }, 10 | }; 11 | 12 | export default defineConfig([ 13 | { 14 | ...baseConfig, 15 | label: 'default', 16 | workspaceFolder: './', 17 | files: 'out/**/*.test.default.js', 18 | }, 19 | { 20 | ...baseConfig, 21 | label: 'pixi', 22 | workspaceFolder: 'fixtures/pixi-workspace/', 23 | files: 'out/**/*.test.pixi.js', 24 | }, 25 | ]); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "out", 5 | "rootDir": "extension", 6 | "sourceMap": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "target": "es6", 10 | "experimentalDecorators": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "esModuleInterop": true, 14 | "isolatedModules": true, 15 | "emitDecoratorMetadata": false 16 | }, 17 | "include": ["extension"], 18 | "exclude": ["node_modules", ".vscode-test"], 19 | "references": [{ "path": "./lsp-proxy" }] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "compile", 7 | "group": "build", 8 | "presentation": { 9 | "panel": "dedicated", 10 | "reveal": "never" 11 | }, 12 | "problemMatcher": ["$tsc"] 13 | }, 14 | { 15 | "type": "npm", 16 | "script": "watch", 17 | "isBackground": true, 18 | "group": { 19 | "kind": "build", 20 | "isDefault": true 21 | }, 22 | "presentation": { 23 | "panel": "dedicated", 24 | "reveal": "never" 25 | }, 26 | "problemMatcher": ["$tsc-watch"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "extensionHost", 9 | "request": "launch", 10 | "name": "Run Extension", 11 | "runtimeExecutable": "${execPath}", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 14 | "preLaunchTask": { 15 | "type": "npm", 16 | "script": "build" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yaml: -------------------------------------------------------------------------------- 1 | name: Set up environment 2 | description: Set up the environment for workflows 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Setup Node 7 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a 8 | with: 9 | node-version: 20 10 | 11 | - name: Cache Node packages 12 | uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 13 | with: 14 | key: npm-${{ hashFiles('package-lock.json', 'lsp-proxy/package-lock.json') }} 15 | path: | 16 | node_modules 17 | lsp-proxy/node_modules 18 | 19 | - name: Install dependencies 20 | shell: bash 21 | run: npm run ci 22 | -------------------------------------------------------------------------------- /extension/types.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | export type Optional = T | undefined; 15 | -------------------------------------------------------------------------------- /extension/debug/constants.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | export const DEBUG_TYPE = 'mojo-lldb'; 15 | -------------------------------------------------------------------------------- /lsp-proxy/src/proxy.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import { MojoLSPProxy } from './MojoLSPProxy'; 15 | 16 | const lspProxy = new MojoLSPProxy(); 17 | lspProxy.start(); 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint extension 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | lint: 16 | name: Lint 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 15 19 | 20 | defaults: 21 | run: 22 | shell: bash 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 27 | 28 | - uses: ./.github/actions/setup 29 | 30 | - name: Run Prettier 31 | run: npx prettier --check . 32 | 33 | - name: Run eslint 34 | run: npx eslint 35 | 36 | - name: Check TypeScript 37 | run: npm run typecheck 38 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - id: check-merge-conflict 8 | 9 | - repo: local 10 | hooks: 11 | - id: prettier 12 | name: format 13 | language: node 14 | entry: prettier --write 15 | types_or: [javascript, ts] 16 | additional_dependencies: 17 | - 'prettier@^3.3.3' 18 | 19 | - id: eslint 20 | name: lint 21 | language: node 22 | entry: npx eslint 23 | types_or: [javascript, ts] 24 | additional_dependencies: 25 | - 'eslint@^9.30.1' 26 | - 'typescript-eslint@^8.35.1' 27 | 28 | - id: tsc 29 | name: type check 30 | language: node 31 | entry: tsc --noEmit -p ./ 32 | pass_filenames: false 33 | types_or: [javascript, ts] 34 | additional_dependencies: 35 | - 'typescript@^4.6.4' 36 | 37 | - id: check-license-headers 38 | name: check license headers 39 | language: python 40 | entry: python ./utils/license.py 41 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build extension 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | workflow_call: 9 | inputs: 10 | ref: 11 | type: string 12 | required: false 13 | 14 | jobs: 15 | build: 16 | name: Build extension 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 15 19 | 20 | defaults: 21 | run: 22 | shell: bash 23 | 24 | outputs: 25 | artifact-id: ${{ steps.upload.outputs.artifact-id }} 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 30 | with: 31 | ref: ${{ inputs.ref || github.sha }} 32 | 33 | - uses: ./.github/actions/setup 34 | 35 | - name: Bundle TypeScript 36 | run: npm run bundle 37 | 38 | - name: Package extension 39 | run: npm run package 40 | 41 | - name: Upload extension as artifact 42 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 43 | id: upload 44 | with: 45 | name: extension-${{ github.sha }} 46 | path: ./out/vscode-mojo.vsix 47 | retention-days: 14 48 | -------------------------------------------------------------------------------- /extension/pyenv.test.pixi.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as assert from 'assert'; 15 | import * as vscode from 'vscode'; 16 | import { extension } from './extension'; 17 | import { SDKKind } from './pyenv'; 18 | 19 | suite('pyenv', function () { 20 | test('should detect Pixi environments', async function () { 21 | await vscode.commands.executeCommand('mojo.extension.restart'); 22 | const sdk = await extension.pyenvManager!.getActiveSDK(); 23 | assert.ok(sdk); 24 | assert.strictEqual(sdk.kind, SDKKind.Environment); 25 | assert.strictEqual(sdk.version, '25.5.0.dev2025071605'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /syntaxes/markdown.syntax.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:text.html.markdown", 4 | "patterns": [ 5 | { 6 | "include": "#mojo-code-block" 7 | } 8 | ], 9 | "repository": { 10 | "mojo-code-block": { 11 | "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(mojo)(\\s+[^`~]*)?$)", 12 | "name": "markup.fenced_code.block.markdown", 13 | "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", 14 | "beginCaptures": { 15 | "3": { 16 | "name": "punctuation.definition.markdown" 17 | }, 18 | "4": { 19 | "name": "fenced_code.block.language.markdown" 20 | }, 21 | "5": { 22 | "name": "fenced_code.block.language.attributes.markdown" 23 | } 24 | }, 25 | "endCaptures": { 26 | "3": { 27 | "name": "punctuation.definition.markdown" 28 | } 29 | }, 30 | "patterns": [ 31 | { 32 | "begin": "(^|\\G)(\\s*)(.*)", 33 | "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", 34 | "contentName": "meta.embedded.block.mojo", 35 | "patterns": [ 36 | { 37 | "include": "source.mojo" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | }, 44 | "scopeName": "markdown.mojo.codeblock" 45 | } 46 | -------------------------------------------------------------------------------- /extension/utils/disposableContext.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as vscode from 'vscode'; 15 | import { Subscription } from 'rxjs'; 16 | 17 | /** 18 | * This class provides a simple wrapper around vscode.Disposable that allows 19 | * for registering additional disposables. 20 | */ 21 | export class DisposableContext implements vscode.Disposable { 22 | private _disposables: vscode.Disposable[] = []; 23 | 24 | constructor() {} 25 | 26 | public dispose() { 27 | for (const disposable of this._disposables) { 28 | disposable.dispose(); 29 | } 30 | this._disposables = []; 31 | } 32 | 33 | /** 34 | * Push an additional disposable to the context. 35 | * 36 | * @param disposable The disposable to register. 37 | */ 38 | public pushSubscription(disposable: vscode.Disposable) { 39 | this._disposables.push(disposable); 40 | } 41 | 42 | public pushRxjsSubscription(subs: Subscription) { 43 | this._disposables.push( 44 | new vscode.Disposable(() => { 45 | subs.unsubscribe(); 46 | }), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # ===----------------------------------------------------------------------=== # 2 | # Copyright (c) 2025, Modular Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | # https://llvm.org/LICENSE.txt 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | # ===----------------------------------------------------------------------=== # 13 | 14 | name: Test extension 15 | 16 | on: 17 | pull_request: 18 | push: 19 | branches: 20 | - main 21 | workflow_dispatch: 22 | 23 | concurrency: 24 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 25 | cancel-in-progress: true 26 | 27 | jobs: 28 | test: 29 | name: Test 30 | runs-on: ubuntu-latest 31 | timeout-minutes: 15 32 | 33 | defaults: 34 | run: 35 | shell: bash 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 40 | 41 | - uses: ./.github/actions/setup 42 | 43 | - name: Setup Pixi 44 | uses: prefix-dev/setup-pixi@273e4808c831936a3ce1a3080c829d9e153143d3 45 | with: 46 | pixi-version: v0.49.0 47 | run-install: false 48 | 49 | - name: Initialize Pixi workspace 50 | working-directory: fixtures/pixi-workspace 51 | run: pixi install --locked 52 | 53 | - name: Build extension 54 | run: npm run build 55 | 56 | - name: Execute tests 57 | run: xvfb-run -a npm test 58 | -------------------------------------------------------------------------------- /lsp-proxy/src/DisposableContext.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import { Disposable } from './types'; 15 | 16 | export class DisposableCallback implements Disposable { 17 | private callback: () => void; 18 | 19 | constructor(callback: () => void) { 20 | this.callback = callback; 21 | } 22 | 23 | dispose(): void { 24 | this.callback(); 25 | } 26 | } 27 | 28 | /** 29 | * This class provides a simple wrapper around `Disposable` that allows for 30 | * registering additional disposables. 31 | * 32 | * Note: We can't use vscode.Disposable because the proxy can't depend on the 33 | * VSCode API. 34 | */ 35 | export class DisposableContext implements Disposable { 36 | private _disposables: Disposable[] = []; 37 | 38 | constructor() {} 39 | 40 | public dispose() { 41 | for (const disposable of this._disposables) { 42 | disposable.dispose(); 43 | } 44 | this._disposables = []; 45 | } 46 | 47 | /** 48 | * Push an additional disposable to the context. 49 | * 50 | * @param disposable The disposable to register. 51 | */ 52 | public pushSubscription(disposable: Disposable) { 53 | this._disposables.push(disposable); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /extension/utils/checkNsight.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as vscode from 'vscode'; 15 | import { Logger } from '../logging'; 16 | 17 | /** 18 | * Check if user has NVIDIA Nsight extension installed. 19 | * Prompts user to install if not installed. 20 | * Returns undefined if extension is installed and enabled. 21 | * Returns an error message string if the extension is not enabled. 22 | */ 23 | export async function checkNsightInstall(logger: Logger) { 24 | const nsight = vscode.extensions.getExtension('nvidia.nsight-vscode-edition'); 25 | if (!nsight) { 26 | // Tell the user to install the nsight extension. 27 | const message = 28 | 'Unable to start the cuda-gdb debug session. You first need to install and enablethe NVIDIA Nsight extension (nsight-vscode-edition).'; 29 | logger.info(message); 30 | const response = await vscode.window.showInformationMessage( 31 | 'Unable to debug with cuda-gdb mode without the NVIDIA Nsight extension.', 32 | 'Find NVIDIA Nsight extension', 33 | ); 34 | if (response) { 35 | vscode.commands.executeCommand( 36 | 'workbench.extensions.search', 37 | '@id:nvidia.nsight-vscode-edition', 38 | ); 39 | } 40 | return message; 41 | } 42 | return false; 43 | } 44 | -------------------------------------------------------------------------------- /extension/utils/vscodeVariables.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as os from 'os'; 15 | import * as path from 'path'; 16 | import * as process from 'process'; 17 | import * as vscode from 'vscode'; 18 | import { Optional } from '../types'; 19 | 20 | /** 21 | * Substitute the given string with some common VSCode variables. 22 | * 23 | * The full list of variable is available in 24 | * https://code.visualstudio.com/docs/editor/variables-reference but we only 25 | * process the ones that don't depend on the open documents or the task runner. 26 | */ 27 | export function substituteVariables( 28 | text: string, 29 | workspaceFolder: Optional, 30 | ) { 31 | text = text.replace( 32 | /\${workspaceFolder}/g, 33 | workspaceFolder?.uri.fsPath || '', 34 | ); 35 | text = text.replace( 36 | /\${workspaceFolderBasename}/g, 37 | path.basename(workspaceFolder?.uri.fsPath || ''), 38 | ); 39 | text = text.replace(/\${userHome}/g, os.homedir()); 40 | text = text.replace(/\${pathSeparator}/g, path.sep); 41 | 42 | while (true) { 43 | const match = text.match(/\${env:([^}]*)}/); 44 | 45 | if (!match) { 46 | break; 47 | } 48 | text = text.replace( 49 | new RegExp(`\\\${env:${match[1]}}`, 'g'), 50 | process.env[match[1]] || '', 51 | ); 52 | } 53 | return text; 54 | } 55 | -------------------------------------------------------------------------------- /extension/utils/config.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as vscode from 'vscode'; 15 | import { Optional } from '../types'; 16 | 17 | /** 18 | * Gets the config value `mojo.`, with an optional workspace folder. 19 | */ 20 | export function get( 21 | key: string, 22 | workspaceFolder: Optional, 23 | ): Optional; 24 | 25 | /** 26 | * Gets the config value `mojo.`, with an optional workspace folder and a 27 | * default value. 28 | */ 29 | export function get( 30 | key: string, 31 | workspaceFolder: Optional, 32 | defaultValue: T, 33 | ): T; 34 | 35 | export function get( 36 | key: string, 37 | workspaceFolder: Optional = undefined, 38 | defaultValue: Optional = undefined, 39 | ): Optional { 40 | if (defaultValue === undefined) { 41 | return vscode.workspace 42 | .getConfiguration('mojo', workspaceFolder) 43 | .get(key); 44 | } 45 | return vscode.workspace 46 | .getConfiguration('mojo', workspaceFolder) 47 | .get(key, defaultValue); 48 | } 49 | 50 | /** 51 | * Sets the config value `mojo.`. 52 | */ 53 | export function update( 54 | key: string, 55 | value: T, 56 | target?: vscode.ConfigurationTarget, 57 | ) { 58 | return vscode.workspace.getConfiguration('mojo').update(key, value, target); 59 | } 60 | -------------------------------------------------------------------------------- /extension/lsp/lsp.test.pixi.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as assert from 'assert'; 15 | import * as vscode from 'vscode'; 16 | import * as path from 'path'; 17 | import { firstValueFrom } from 'rxjs'; 18 | import { extension } from '../extension'; 19 | 20 | const repoConfig = { 21 | fixtures: path.join(__dirname, '..', '..', 'fixtures'), 22 | }; 23 | 24 | suite('LSP', function () { 25 | test('LSP should not be loaded on startup', async function () { 26 | // Restart the extension. Tests run in a shared environment, so if other tests 27 | // have created the LSP, this test will fail otherwise. 28 | await vscode.commands.executeCommand('mojo.extension.restart'); 29 | 30 | assert.strictEqual(extension.lspManager!.lspClient, undefined); 31 | }); 32 | 33 | test('LSP should be launched when a Mojo file is opened', async function () { 34 | // Restart the extension. Tests run in a shared environment, so if other tests 35 | // have created the LSP, this test will fail otherwise. 36 | await vscode.commands.executeCommand('mojo.extension.restart'); 37 | 38 | const lsp = firstValueFrom(extension.lspManager!.lspClientChanges); 39 | 40 | await vscode.workspace.openTextDocument( 41 | vscode.Uri.file( 42 | path.join(repoConfig.fixtures, 'pixi-workspace', 'main.mojo'), 43 | ), 44 | ); 45 | 46 | assert.strictEqual((await lsp)!.name, 'Mojo Language Client'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /extension/debug/types.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | export type FrameId = number; 15 | export type RequestId = number; 16 | export type SessionId = string; 17 | /** 18 | * This is a display name generated by LLDB that attempts to be unique. 19 | */ 20 | export type VariableDisplayName = string; 21 | /** 22 | * This name is an LLDB variable path that could collide in case of shadowing. 23 | */ 24 | export type VariableEvaluateName = string; 25 | 26 | export type Variable = { 27 | name: VariableDisplayName; 28 | evaluateName: VariableEvaluateName; 29 | value: string; 30 | /** 31 | * Extension to the protocol. 32 | */ 33 | $__lldb_extensions: { 34 | /** 35 | * A summary generated by lldb-dap if `SBValue` doesn't have an actual 36 | * summary. 37 | */ 38 | autoSummary?: string; 39 | /** 40 | * A standard summary generated by `SBValue`. 41 | */ 42 | summary?: string; 43 | /** 44 | * An error message if it was not possible to generate a value or a summary. 45 | */ 46 | error?: string; 47 | /** 48 | * The source declaration of the variable. 49 | */ 50 | declaration?: { path?: string; line?: number; column?: number }; 51 | }; 52 | }; 53 | 54 | export type DAPScopesRequest = { 55 | command: 'scopes'; 56 | seq: RequestId; 57 | arguments: { frameId: FrameId }; 58 | }; 59 | 60 | export type DAPVariablesRequest = { 61 | command: 'variables'; 62 | seq: RequestId; 63 | arguments: { variablesReference: number }; 64 | }; 65 | 66 | export type DAPVariablesResponse = { 67 | command: 'variables'; 68 | request_seq: RequestId; 69 | body: { variables: Variable[] }; 70 | }; 71 | -------------------------------------------------------------------------------- /extension/lsp/recorder.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import { DisposableContext } from '../utils/disposableContext'; 15 | import * as vscode from 'vscode'; 16 | import { MessageSignature, CancellationToken } from 'vscode-languageclient'; 17 | import { createWriteStream, WriteStream } from 'fs'; 18 | 19 | export class LSPRecorder extends DisposableContext { 20 | private output: WriteStream; 21 | 22 | constructor(outPath: string) { 23 | super(); 24 | 25 | this.output = createWriteStream(outPath); 26 | 27 | this.pushSubscription( 28 | new vscode.Disposable(() => { 29 | this.output.close(); 30 | }), 31 | ); 32 | } 33 | 34 | // Follows GeneralMiddleware implementation from vscode-languageclient. 35 | public sendRequest( 36 | type: string | MessageSignature, 37 | param: P | undefined, 38 | token: CancellationToken | undefined, 39 | next: ( 40 | type: string | MessageSignature, 41 | param?: P, 42 | token?: CancellationToken, 43 | ) => Promise, 44 | ): Promise { 45 | const message = { 46 | type: 'request', 47 | method: type, 48 | param: param, 49 | }; 50 | 51 | this.output.write(JSON.stringify(message)); 52 | this.output.write('\n'); 53 | return next(type, param, token); 54 | } 55 | 56 | public sendNotification

( 57 | type: string | MessageSignature, 58 | next: (type: string | MessageSignature, params?: P) => Promise, 59 | param: P, 60 | ): Promise { 61 | const message = { 62 | type: 'notification', 63 | method: type, 64 | param: param, 65 | }; 66 | 67 | this.output.write(JSON.stringify(message)); 68 | this.output.write('\n'); 69 | return next(type, param); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/cla.yaml: -------------------------------------------------------------------------------- 1 | # ===----------------------------------------------------------------------=== # 2 | # Copyright (c) 2025, Modular Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | # https://llvm.org/LICENSE.txt 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | # ===----------------------------------------------------------------------=== # 13 | 14 | name: 'CLA Assistant' 15 | on: 16 | issue_comment: 17 | types: [created] 18 | pull_request_target: 19 | types: [opened, closed, synchronize] 20 | 21 | # explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings 22 | permissions: 23 | actions: write 24 | contents: read 25 | pull-requests: write 26 | statuses: write 27 | 28 | jobs: 29 | CLAAssistant: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e 33 | id: app-token 34 | with: 35 | app-id: ${{ vars.CLA_BOT_CLIENT_ID }} 36 | private-key: ${{ secrets.CLA_BOT_PRIVATE_KEY }} 37 | owner: modular 38 | repositories: cla 39 | permission-contents: write 40 | 41 | - name: 'CLA Assistant' 42 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 43 | uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | # the below token should have repo scope and must be manually added by you in the repository's secret 47 | # This token is required only if you have configured to store the signatures in a remote repository/organization 48 | PERSONAL_ACCESS_TOKEN: ${{ steps.app-token.outputs.token }} 49 | with: 50 | path-to-document: 'https://github.com/modular/cla/blob/main/CLA.md' # e.g. a CLA or a DCO document 51 | branch: 'main' 52 | remote-organization-name: 'modular' 53 | remote-repository-name: 'cla' 54 | path-to-signatures: 'signatures/version1/cla.json' 55 | -------------------------------------------------------------------------------- /extension/formatter.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import { execFile } from 'child_process'; 15 | import * as vscode from 'vscode'; 16 | 17 | import { get } from './utils/config'; 18 | import { PythonEnvironmentManager } from './pyenv'; 19 | import { Logger } from './logging'; 20 | 21 | export function registerFormatter( 22 | envManager: PythonEnvironmentManager, 23 | logger: Logger, 24 | ) { 25 | return vscode.languages.registerDocumentFormattingEditProvider('mojo', { 26 | async provideDocumentFormattingEdits(document, _options) { 27 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); 28 | const backupFolder = vscode.workspace.workspaceFolders?.[0]; 29 | const cwd = workspaceFolder?.uri?.fsPath || backupFolder?.uri.fsPath; 30 | const args = get('formatting.args', workspaceFolder, []); 31 | 32 | const sdk = await envManager.getActiveSDK(); 33 | 34 | if (!sdk) { 35 | return []; 36 | } 37 | 38 | return new Promise(function (resolve, reject) { 39 | const originalDocumentText = document.getText(); 40 | const process = execFile( 41 | sdk.mblackPath, 42 | ['--fast', '--preview', '--quiet', '-t', 'mojo', ...args, '-'], 43 | { cwd, env: sdk.getProcessEnv() }, 44 | (error, stdout, stderr) => { 45 | // Process any errors/warnings during formatting. These aren't all 46 | // necessarily fatal, so this doesn't prevent edits from being 47 | // applied. 48 | if (error) { 49 | logger.error(`Formatting error:\n${stderr}`); 50 | reject(error); 51 | return; 52 | } 53 | 54 | // Formatter returned nothing, don't try to apply any edits. 55 | if (originalDocumentText.length > 0 && stdout.length === 0) { 56 | resolve([]); 57 | return; 58 | } 59 | 60 | // Otherwise, the formatter returned the formatted text. Update the 61 | // document. 62 | const documentRange = new vscode.Range( 63 | document.lineAt(0).range.start, 64 | document.lineAt( 65 | document.lineCount - 1, 66 | ).rangeIncludingLineBreak.end, 67 | ); 68 | resolve([new vscode.TextEdit(documentRange, stdout)]); 69 | }, 70 | ); 71 | 72 | process.stdin?.write(originalDocumentText); 73 | process.stdin?.end(); 74 | }); 75 | }, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /lsp-proxy/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mojo-lsp-proxy", 3 | "version": "v2024.5.1003", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "mojo-lsp-proxy", 9 | "version": "v2024.5.1003", 10 | "dependencies": { 11 | "rxjs": "7.8.1", 12 | "vscode-languageserver": "9.0.1", 13 | "vscode-languageserver-protocol": "3.17.5", 14 | "vscode-languageserver-textdocument": "1.0.4" 15 | }, 16 | "engines": { 17 | "node": "*" 18 | } 19 | }, 20 | "node_modules/rxjs": { 21 | "version": "7.8.1", 22 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", 23 | "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", 24 | "dependencies": { 25 | "tslib": "^2.1.0" 26 | } 27 | }, 28 | "node_modules/tslib": { 29 | "version": "2.6.2", 30 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 31 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 32 | }, 33 | "node_modules/vscode-jsonrpc": { 34 | "version": "8.2.0", 35 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", 36 | "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", 37 | "engines": { 38 | "node": ">=14.0.0" 39 | } 40 | }, 41 | "node_modules/vscode-languageserver": { 42 | "version": "9.0.1", 43 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", 44 | "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", 45 | "dependencies": { 46 | "vscode-languageserver-protocol": "3.17.5" 47 | }, 48 | "bin": { 49 | "installServerIntoExtension": "bin/installServerIntoExtension" 50 | } 51 | }, 52 | "node_modules/vscode-languageserver-protocol": { 53 | "version": "3.17.5", 54 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", 55 | "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", 56 | "dependencies": { 57 | "vscode-jsonrpc": "8.2.0", 58 | "vscode-languageserver-types": "3.17.5" 59 | } 60 | }, 61 | "node_modules/vscode-languageserver-textdocument": { 62 | "version": "1.0.4", 63 | "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz", 64 | "integrity": "sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ==" 65 | }, 66 | "node_modules/vscode-languageserver-types": { 67 | "version": "3.17.5", 68 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", 69 | "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /extension/telemetry.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | // 14 | // Implements a wrapper around @vscode/extension-telemetry to allow us more 15 | // control over the telemetry reporting process. 16 | // 17 | //===----------------------------------------------------------------------===// 18 | 19 | import * as vscode from 'vscode'; 20 | 21 | import BaseTelemetryReporter from '@vscode/extension-telemetry'; 22 | import { 23 | TelemetryEventProperties, 24 | TelemetryEventMeasurements, 25 | } from '@vscode/extension-telemetry'; 26 | 27 | export class TelemetryReporter implements vscode.Disposable { 28 | /** 29 | * Enables or disables telemetry reporting. If this flag is set to `false`, 30 | * calls to telemetry reporting methods will become no-ops. 31 | */ 32 | get enabled() { 33 | return this.reporter !== undefined && this._enabled; 34 | } 35 | 36 | set enabled(value) { 37 | this._enabled = value; 38 | } 39 | 40 | private _enabled: boolean = true; 41 | 42 | private reporter?: BaseTelemetryReporter; 43 | private additionalProperties: { [key: string]: string } = {}; 44 | private additionalMeasurements: { [key: string]: number } = {}; 45 | 46 | /** 47 | * Creates a new telemetry reporter. If the connection string is undefined, 48 | * the reporter will be permanently disabled and will never report telemetry 49 | * to the remote server. 50 | */ 51 | constructor(connectionString?: string) { 52 | if (connectionString) { 53 | this.reporter = new BaseTelemetryReporter(connectionString); 54 | } 55 | } 56 | 57 | public sendTelemetryEvent( 58 | eventName: string, 59 | properties?: TelemetryEventProperties, 60 | measurements?: TelemetryEventMeasurements, 61 | ): void { 62 | if (!this.enabled || !this.reporter) { 63 | return; 64 | } 65 | 66 | this.reporter.sendTelemetryEvent( 67 | eventName, 68 | { ...properties, ...this.additionalProperties }, 69 | { ...measurements, ...this.additionalMeasurements }, 70 | ); 71 | } 72 | 73 | public sendTelemetryErrorEvent( 74 | eventName: string, 75 | properties?: TelemetryEventProperties, 76 | measurements?: TelemetryEventMeasurements, 77 | ): void { 78 | if (!this.enabled || !this.reporter) { 79 | return; 80 | } 81 | 82 | this.reporter.sendTelemetryErrorEvent( 83 | eventName, 84 | { ...properties, ...this.additionalProperties }, 85 | { ...measurements, ...this.additionalMeasurements }, 86 | ); 87 | } 88 | 89 | public dispose() { 90 | if (this.reporter) { 91 | this.reporter.dispose(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#", 4 | "blockComment": ["\"\"\"", "\"\"\""] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"] 10 | ], 11 | "autoClosingPairs": [ 12 | { 13 | "open": "{", 14 | "close": "}" 15 | }, 16 | { 17 | "open": "[", 18 | "close": "]" 19 | }, 20 | { 21 | "open": "(", 22 | "close": ")" 23 | }, 24 | { 25 | "open": "\"", 26 | "close": "\"", 27 | "notIn": ["string"] 28 | }, 29 | { 30 | "open": "r\"", 31 | "close": "\"", 32 | "notIn": ["string", "comment"] 33 | }, 34 | { 35 | "open": "R\"", 36 | "close": "\"", 37 | "notIn": ["string", "comment"] 38 | }, 39 | { 40 | "open": "u\"", 41 | "close": "\"", 42 | "notIn": ["string", "comment"] 43 | }, 44 | { 45 | "open": "U\"", 46 | "close": "\"", 47 | "notIn": ["string", "comment"] 48 | }, 49 | { 50 | "open": "f\"", 51 | "close": "\"", 52 | "notIn": ["string", "comment"] 53 | }, 54 | { 55 | "open": "F\"", 56 | "close": "\"", 57 | "notIn": ["string", "comment"] 58 | }, 59 | { 60 | "open": "b\"", 61 | "close": "\"", 62 | "notIn": ["string", "comment"] 63 | }, 64 | { 65 | "open": "B\"", 66 | "close": "\"", 67 | "notIn": ["string", "comment"] 68 | }, 69 | { 70 | "open": "'", 71 | "close": "'", 72 | "notIn": ["string", "comment"] 73 | }, 74 | { 75 | "open": "r'", 76 | "close": "'", 77 | "notIn": ["string", "comment"] 78 | }, 79 | { 80 | "open": "R'", 81 | "close": "'", 82 | "notIn": ["string", "comment"] 83 | }, 84 | { 85 | "open": "u'", 86 | "close": "'", 87 | "notIn": ["string", "comment"] 88 | }, 89 | { 90 | "open": "U'", 91 | "close": "'", 92 | "notIn": ["string", "comment"] 93 | }, 94 | { 95 | "open": "f'", 96 | "close": "'", 97 | "notIn": ["string", "comment"] 98 | }, 99 | { 100 | "open": "F'", 101 | "close": "'", 102 | "notIn": ["string", "comment"] 103 | }, 104 | { 105 | "open": "b'", 106 | "close": "'", 107 | "notIn": ["string", "comment"] 108 | }, 109 | { 110 | "open": "B'", 111 | "close": "'", 112 | "notIn": ["string", "comment"] 113 | }, 114 | { 115 | "open": "`", 116 | "close": "`", 117 | "notIn": ["string"] 118 | } 119 | ], 120 | "surroundingPairs": [ 121 | ["{", "}"], 122 | ["[", "]"], 123 | ["(", ")"], 124 | ["\"", "\""], 125 | ["'", "'"], 126 | ["`", "`"] 127 | ], 128 | "folding": { 129 | "offSide": true, 130 | "markers": { 131 | "start": "^\\s*#\\s*region\\b", 132 | "end": "^\\s*#\\s*endregion\\b" 133 | } 134 | }, 135 | "onEnterRules": [ 136 | { 137 | "beforeText": "^\\s*(?:def|class|fn|struct|trait|for|if|elif|else|while|try|with|finally|except|async).*?:\\s*$", 138 | "action": { 139 | "indent": "indent" 140 | } 141 | } 142 | ] 143 | } 144 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | In the interest of fostering an open and welcoming environment, we as 4 | contributors and maintainers pledge to make participation in our project and 5 | our community a harassment-free experience for everyone, regardless of age, 6 | body size, disability, ethnicity, gender identity and expression, level of 7 | experience, nationality, personal appearance, race, religion, or sexual 8 | identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | All community forums and spaces are meant for professional interactions that 13 | are friendly, inclusive, helpful, and collaborative. Examples of behavior that 14 | contributes to creating a positive environment include: 15 | 16 | - Using welcoming and inclusive language. 17 | - Being respectful of differing viewpoints and experiences. 18 | - Gracefully accepting constructive criticism. 19 | - Focusing on what is best for the community. 20 | - Showing empathy towards other community members. 21 | 22 | Any behavior that could reasonably be considered inappropriate in a 23 | professional setting is unacceptable. Examples of unacceptable behavior by 24 | participants include: 25 | 26 | - The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances. 28 | - Trolling, insulting/derogatory comments, and personal or political attacks. 29 | - Public or private harassment. 30 | - Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission. 32 | - Conduct which could reasonably be considered inappropriate for the forum in 33 | which it occurs. 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies to all project content and public spaces on the 50 | MAX GitHub repo, the rest of Modular’s GitHub organization, and all other 51 | official MAX community spaces and communication mediums, whether offline or 52 | online. 53 | 54 | ## Enforcement 55 | 56 | Instances of abusive, harassment, or otherwise unacceptable behavior should be 57 | reported to the project team at . All complaints will 58 | be reviewed and investigated and will result in a response that is deemed 59 | necessary and appropriate to the circumstances. The project team is obligated 60 | to maintain confidentiality with regard to the reporter of an incident. Further 61 | details of specific enforcement policies may be posted separately. 62 | 63 | Project maintainers who do not follow or enforce the Code of Conduct in good 64 | faith may face temporary or permanent repercussions as determined by other 65 | members of the project's leadership. 66 | 67 | ## Attribution 68 | 69 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 70 | available at , and includes some 71 | aspects of the Geek Feminism Code of Conduct and the Drupal Code of Conduct. 72 | -------------------------------------------------------------------------------- /extension/test/reporter.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import { reporters, Runner, Test } from 'mocha'; 15 | 16 | import { setLogHook } from '../extension'; 17 | 18 | const { 19 | EVENT_TEST_BEGIN, 20 | EVENT_SUITE_BEGIN, 21 | EVENT_SUITE_END, 22 | EVENT_TEST_PASS, 23 | EVENT_TEST_PENDING, 24 | EVENT_TEST_FAIL, 25 | EVENT_RUN_END, 26 | } = Runner.constants; 27 | const Base = reporters.Base; 28 | 29 | class VsCodeReporter extends Base { 30 | constructor(runner: Runner) { 31 | super(runner); 32 | 33 | const testLogs: { [id: string]: string[] } = {}; 34 | let indentDepth = 0; 35 | 36 | const indent = (str: string) => { 37 | let result = ''; 38 | for (const line of str.split('\n')) { 39 | result += ' '.repeat(indentDepth) + line + '\n'; 40 | } 41 | return result.trimEnd(); 42 | }; 43 | 44 | runner.on(EVENT_SUITE_BEGIN, (suite) => { 45 | console.log(indent(Base.color('suite', suite.title))); 46 | indentDepth += 2; 47 | }); 48 | 49 | runner.on(EVENT_SUITE_END, () => { 50 | indentDepth -= 2; 51 | }); 52 | 53 | runner.on(EVENT_TEST_BEGIN, (test: Test) => { 54 | const testId = test.fullTitle(); 55 | testLogs[testId] = []; 56 | 57 | const callback = (level: string, message: string) => 58 | testLogs[testId].push(`[${level.padEnd(5, ' ')}]: ${message}`); 59 | setLogHook(callback); 60 | }); 61 | 62 | runner.on(EVENT_TEST_PASS, (test: Test) => { 63 | console.log( 64 | indent( 65 | `${Base.color('checkmark', Base.symbols.ok)} ${Base.color('pass', test.title)}`, 66 | ), 67 | ); 68 | }); 69 | 70 | runner.on(EVENT_TEST_FAIL, (test: Test, err) => { 71 | console.log( 72 | indent( 73 | `${Base.color('fail', Base.symbols.err)} ${Base.color('fail', test.title)}`, 74 | ), 75 | ); 76 | indentDepth += 2; 77 | 78 | if (err.stack) { 79 | console.log(indent(`${Base.color('error stack', err.stack)}`)); 80 | } else { 81 | console.log(indent(`${Base.color('error title', err.name)}`)); 82 | console.log(indent(`${Base.color('error message', err.message)}`)); 83 | } 84 | 85 | console.log(indent('\nExtension logs:\n')); 86 | 87 | const logs = testLogs[test.fullTitle()]; 88 | for (const log of logs) { 89 | console.log(indent(log)); 90 | } 91 | 92 | indentDepth -= 2; 93 | }); 94 | 95 | runner.on(EVENT_TEST_PENDING, (test: Test) => { 96 | console.log(indent(Base.color('pending', `- ${test.title}`))); 97 | }); 98 | 99 | runner.on(EVENT_RUN_END, () => { 100 | this.epilogue(); 101 | }); 102 | } 103 | } 104 | 105 | module.exports = VsCodeReporter; 106 | -------------------------------------------------------------------------------- /utils/license.py: -------------------------------------------------------------------------------- 1 | # ===----------------------------------------------------------------------=== # 2 | # Copyright (c) 2025, Modular Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | # https://llvm.org/LICENSE.txt 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | # ===----------------------------------------------------------------------=== # 13 | 14 | 15 | import sys 16 | from pathlib import Path 17 | 18 | 19 | JS_LICENSE_TEXT = """ 20 | //===----------------------------------------------------------------------===// 21 | // Copyright (c) 2025, Modular Inc. All rights reserved. 22 | // 23 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 24 | // https://llvm.org/LICENSE.txt 25 | // 26 | // Unless required by applicable law or agreed to in writing, software 27 | // distributed under the License is distributed on an "AS IS" BASIS, 28 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | // See the License for the specific language governing permissions and 30 | // limitations under the License. 31 | //===----------------------------------------------------------------------===// 32 | """.strip() 33 | 34 | PY_YAML_LICENSE_TEXT = """ 35 | # ===----------------------------------------------------------------------=== # 36 | # Copyright (c) 2025, Modular Inc. All rights reserved. 37 | # 38 | # Licensed under the Apache License v2.0 with LLVM Exceptions: 39 | # https://llvm.org/LICENSE.txt 40 | # 41 | # Unless required by applicable law or agreed to in writing, software 42 | # distributed under the License is distributed on an "AS IS" BASIS, 43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 44 | # See the License for the specific language governing permissions and 45 | # limitations under the License. 46 | # ===----------------------------------------------------------------------=== # 47 | """.strip() 48 | 49 | EXCLUDES = [Path(p) for p in [ 50 | ".pre-commit-config.yaml", 51 | "package.json", 52 | "package-lock.json", 53 | "language-configuration.json", 54 | "extension/external/psList.ts", 55 | "extension/server/RpcServer.ts", 56 | "extension/logging.ts", 57 | "esbuild.mjs", 58 | "eslint.config.mjs", 59 | ".vscode-test.mjs", 60 | ]] 61 | 62 | 63 | def check_file(path: Path): 64 | with open(path, "r") as f: 65 | contents = f.read().strip() 66 | 67 | if path in EXCLUDES: 68 | return True 69 | 70 | if path.suffix in [".js", ".ts", ".mjs"] and not contents.startswith(JS_LICENSE_TEXT): 71 | return False 72 | elif path.suffix in [".py", ".mojo", ".yaml"] and not contents.startswith(PY_YAML_LICENSE_TEXT): 73 | return False 74 | 75 | return True 76 | 77 | 78 | def main(): 79 | failing = [] 80 | 81 | for arg in sys.argv[1:]: 82 | if not Path(arg).name.endswith((".js", ".ts", ".mjs")): 83 | continue 84 | 85 | if not check_file(Path(arg)): 86 | failing.append(arg) 87 | 88 | if len(failing) > 0: 89 | print("Missing license headers") 90 | for f in failing: 91 | print(f) 92 | 93 | sys.exit(1) 94 | 95 | 96 | if __name__ == "__main__": 97 | main() 98 | -------------------------------------------------------------------------------- /lsp-proxy/src/types.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import type { 15 | CodeActionParams, 16 | CompletionParams, 17 | DefinitionParams, 18 | DocumentSymbolParams, 19 | FoldingRangeParams, 20 | HoverParams, 21 | InitializeParams, 22 | InlayHintParams, 23 | ReferenceParams, 24 | SemanticTokensDeltaParams, 25 | SemanticTokensParams, 26 | SignatureHelpParams, 27 | } from 'vscode-languageserver-protocol'; 28 | import type { createConnection as createClientConnection } from 'vscode-languageserver/node'; 29 | 30 | /** 31 | * A generic disposable. 32 | * 33 | * Note: We can't use vscode.Disposable because the proxy can't depend on the 34 | * VSCode API. 35 | */ 36 | export interface Disposable { 37 | dispose(): void; 38 | } 39 | /** 40 | * Type alias for a URI. 41 | */ 42 | export type URI = string; 43 | 44 | /** 45 | * The type that represents a connection with the VSCode LSP client. 46 | */ 47 | export type Client = ReturnType; 48 | 49 | /** 50 | * This type represents the initialization options send by the extension to the 51 | * proxy. 52 | */ 53 | export interface InitializationOptions { 54 | /** 55 | * The path to `mojo-lsp-server`. 56 | */ 57 | serverPath: string; 58 | /** 59 | * The arguments to use when invoking `mojo-lsp-server`. 60 | */ 61 | serverArgs: string[]; 62 | /** 63 | * The environment to use when invoking `mojo-lsp-server`. 64 | */ 65 | serverEnv: { [env: string]: Optional }; 66 | } 67 | 68 | /** 69 | * This type represents a decoded JSON object. 70 | */ 71 | export type JSONObject = { 72 | [key: string]: any; 73 | }; 74 | 75 | /** 76 | * A simple type alias for a LSP request id. 77 | */ 78 | export type RequestId = number; 79 | 80 | /** 81 | * This type contains a process exit information. At least one of these two 82 | * fields is guaranteed not to be null. 83 | */ 84 | export type ExitStatus = { 85 | code: number | null; 86 | signal: NodeJS.Signals | null; 87 | }; 88 | 89 | // The `shutdown` request doesn't have params, but using `unknown` simplifies 90 | // typechecking. 91 | export type ShutdownParams = unknown; 92 | 93 | /** 94 | * A custom request sent by the extension to record all traffic sent to 95 | * the underlying language server in a replayable form. 96 | */ 97 | export type RecordSessionParams = { 98 | /* 99 | * Whether to enable or disable session recording. 100 | */ 101 | enabled: boolean; 102 | }; 103 | 104 | /** 105 | * This union type represents all supported request params that contain a 106 | * `textDocument` entry. 107 | */ 108 | export type RequestParamsWithDocument = 109 | | CodeActionParams 110 | | CompletionParams 111 | | DefinitionParams 112 | | DocumentSymbolParams 113 | | FoldingRangeParams 114 | | HoverParams 115 | | ReferenceParams 116 | | SignatureHelpParams 117 | | InlayHintParams 118 | | SemanticTokensParams 119 | | SemanticTokensDeltaParams; 120 | 121 | /** 122 | * This union type represents all supported request params that don't contain a 123 | * `textDocument` entry. 124 | */ 125 | export type RequestParamsWithoutDocument = InitializeParams | ShutdownParams; 126 | 127 | /** 128 | * This union type represents all supported request params. 129 | */ 130 | export type RequestParams = 131 | | RequestParamsWithDocument 132 | | RequestParamsWithoutDocument; 133 | 134 | export type Optional = T | undefined; 135 | -------------------------------------------------------------------------------- /extension/logging.test.default.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as assert from 'assert'; 15 | import { LogChannel, LogLevel } from './logging'; 16 | 17 | function createLogSpy(): [string[], (level: string, message: string) => void] { 18 | const lines: string[] = []; 19 | return [ 20 | lines, 21 | (_level: string, message: string) => { 22 | lines.push(message); 23 | }, 24 | ]; 25 | } 26 | 27 | suite('Logging', () => { 28 | test('logs should respect output levels', () => { 29 | const channel = new LogChannel('Test Channel'); 30 | const [lines, callback] = createLogSpy(); 31 | channel.logCallback = callback; 32 | 33 | channel.setOutputLevel(LogLevel.None); 34 | channel.error('error'); 35 | channel.warn('warn'); 36 | channel.info('info'); 37 | channel.debug('debug'); 38 | channel.trace('trace'); 39 | assert.deepStrictEqual(lines, []); 40 | lines.length = 0; 41 | 42 | channel.setOutputLevel(LogLevel.Error); 43 | channel.error('error'); 44 | channel.warn('warn'); 45 | channel.info('info'); 46 | channel.debug('debug'); 47 | channel.trace('trace'); 48 | assert.deepStrictEqual(lines, ['error']); 49 | lines.length = 0; 50 | 51 | channel.setOutputLevel(LogLevel.Warn); 52 | channel.error('error'); 53 | channel.warn('warn'); 54 | channel.info('info'); 55 | channel.debug('debug'); 56 | channel.trace('trace'); 57 | assert.deepStrictEqual(lines, ['error', 'warn']); 58 | lines.length = 0; 59 | 60 | channel.setOutputLevel(LogLevel.Info); 61 | channel.error('error'); 62 | channel.warn('warn'); 63 | channel.info('info'); 64 | channel.debug('debug'); 65 | channel.trace('trace'); 66 | assert.deepStrictEqual(lines, ['error', 'warn', 'info']); 67 | lines.length = 0; 68 | 69 | channel.setOutputLevel(LogLevel.Debug); 70 | channel.error('error'); 71 | channel.warn('warn'); 72 | channel.info('info'); 73 | channel.debug('debug'); 74 | channel.trace('trace'); 75 | assert.deepStrictEqual(lines, ['error', 'warn', 'info', 'debug']); 76 | lines.length = 0; 77 | 78 | channel.setOutputLevel(LogLevel.Trace); 79 | channel.error('error'); 80 | channel.warn('warn'); 81 | channel.info('info'); 82 | channel.debug('debug'); 83 | channel.trace('trace'); 84 | assert.deepStrictEqual(lines, ['error', 'warn', 'info', 'debug', 'trace']); 85 | lines.length = 0; 86 | }); 87 | 88 | test('data should be logged as JSON', () => { 89 | const channel = new LogChannel('Test Channel'); 90 | const [lines, callback] = createLogSpy(); 91 | channel.logCallback = callback; 92 | 93 | channel.setOutputLevel(LogLevel.Info); 94 | 95 | channel.info('message', { foo: 123, bar: true, baz: [1, 2, 3] }); 96 | assert.equal(lines.length, 2); 97 | assert.equal(lines[0], 'message'); 98 | 99 | const json = JSON.parse(lines[1]); 100 | assert.deepStrictEqual(json, { 101 | foo: 123, 102 | bar: true, 103 | baz: [1, 2, 3], 104 | }); 105 | }); 106 | 107 | test('data should respect log level', () => { 108 | const channel = new LogChannel('Test Channel'); 109 | const [lines, callback] = createLogSpy(); 110 | channel.logCallback = callback; 111 | channel.setOutputLevel(LogLevel.Info); 112 | 113 | channel.debug('message', { foo: 123 }); 114 | assert.deepStrictEqual(lines, []); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /extension/utils/files.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as path from 'path'; 15 | import * as vscode from 'vscode'; 16 | import { Optional } from '../types'; 17 | 18 | /** 19 | * Utility class for handling files relative to their containing workspace 20 | * folder. 21 | */ 22 | export class WorkspaceAwareFile { 23 | uri: vscode.Uri; 24 | workspaceFolder?: vscode.WorkspaceFolder; 25 | /** 26 | * The path relative to its containing workspace folder, or the full file 27 | * system path if no workspace folder contains it. If it's a relative path, it 28 | * is prepended by the name of the workspace folder. 29 | */ 30 | relativePath: string; 31 | baseName: string; 32 | 33 | constructor(uri: vscode.Uri) { 34 | this.uri = uri; 35 | this.baseName = path.basename(uri.fsPath); 36 | this.relativePath = vscode.workspace.asRelativePath( 37 | this.uri, 38 | /*includeWorkspaceFolder=*/ true, 39 | ); 40 | } 41 | } 42 | 43 | export function isMojoFile(uri: Optional): boolean { 44 | return ( 45 | uri !== undefined && 46 | (uri.fsPath.endsWith('.mojo') || uri.fsPath.endsWith('.🔥')) 47 | ); 48 | } 49 | 50 | /** 51 | * @returns All the currently open Mojo files as tuple, where the first element 52 | * is the active document if it's a mojo file, and the second element are 53 | * all other mojo files in no particular order. 54 | */ 55 | export function getAllOpenMojoFiles(): [ 56 | Optional, 57 | WorkspaceAwareFile[], 58 | ] { 59 | const activeRawUri = vscode.window.activeTextEditor?.document.uri; 60 | const activeFile = 61 | activeRawUri && isMojoFile(activeRawUri) 62 | ? new WorkspaceAwareFile(activeRawUri) 63 | : undefined; 64 | 65 | const otherOpenFiles = vscode.window.tabGroups.all 66 | .flatMap((tabGroup) => tabGroup.tabs) 67 | .map((tab) => (tab.input as any)?.uri) 68 | .filter(isMojoFile) 69 | .map((uri) => new WorkspaceAwareFile(uri)) 70 | // We remove the active file from this list. 71 | .filter( 72 | (file) => !activeFile || file.uri.toString() != activeFile.uri.toString(), 73 | ); 74 | 75 | return [activeFile, otherOpenFiles]; 76 | } 77 | 78 | export async function directoryExists(path: string): Promise { 79 | try { 80 | const stat = await vscode.workspace.fs.stat(vscode.Uri.file(path)); 81 | if (stat.type & vscode.FileType.Directory) { 82 | return true; 83 | } 84 | } catch (e) { 85 | console.error(e); 86 | } 87 | return false; 88 | } 89 | 90 | export async function fileExists(path: string): Promise { 91 | try { 92 | const stat = await vscode.workspace.fs.stat(vscode.Uri.file(path)); 93 | if (stat.type & (vscode.FileType.File | vscode.FileType.SymbolicLink)) { 94 | return true; 95 | } 96 | } catch (e) { 97 | console.error(e); 98 | } 99 | return false; 100 | } 101 | 102 | export async function readFile(path: string): Promise> { 103 | try { 104 | return new TextDecoder().decode( 105 | await vscode.workspace.fs.readFile(vscode.Uri.file(path)), 106 | ); 107 | } catch { 108 | return undefined; 109 | } 110 | } 111 | 112 | export async function writeFile( 113 | path: string, 114 | contents: string, 115 | ): Promise { 116 | try { 117 | await vscode.workspace.fs.writeFile( 118 | vscode.Uri.file(path), 119 | new TextEncoder().encode(contents), 120 | ); 121 | return true; 122 | } catch { 123 | return false; 124 | } 125 | } 126 | 127 | export async function moveUpUntil( 128 | fsPath: string, 129 | condition: (p: string) => Promise, 130 | ): Promise> { 131 | while (fsPath.length > 0) { 132 | if (await condition(fsPath)) { 133 | return fsPath; 134 | } 135 | const dirname = path.dirname(fsPath); 136 | if (dirname === fsPath) { 137 | break; 138 | } 139 | fsPath = dirname; 140 | } 141 | return undefined; 142 | } 143 | -------------------------------------------------------------------------------- /extension/utils/configWatcher.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as chokidar from 'chokidar'; 15 | import * as vscode from 'vscode'; 16 | 17 | import * as config from './config'; 18 | import { DisposableContext } from './disposableContext'; 19 | import { Optional } from '../types'; 20 | 21 | /** 22 | * Prompt the user to see if we should restart the server. 23 | */ 24 | async function promptRestart(settingName: string, promptMessage: string) { 25 | switch (config.get(settingName, /*workspaceFolder=*/ undefined)) { 26 | case 'restart': 27 | vscode.commands.executeCommand('mojo.extension.restart'); 28 | break; 29 | case 'ignore': 30 | break; 31 | case 'prompt': 32 | default: 33 | switch ( 34 | await vscode.window.showInformationMessage( 35 | promptMessage, 36 | 'Yes', 37 | 'Yes, always', 38 | 'No, never', 39 | ) 40 | ) { 41 | case 'Yes': 42 | vscode.commands.executeCommand('mojo.extension.restart'); 43 | break; 44 | case 'Yes, always': 45 | vscode.commands.executeCommand('mojo.extension.restart'); 46 | config.update( 47 | settingName, 48 | 'restart', 49 | vscode.ConfigurationTarget.Global, 50 | ); 51 | break; 52 | case 'No, never': 53 | config.update( 54 | settingName, 55 | 'ignore', 56 | vscode.ConfigurationTarget.Global, 57 | ); 58 | break; 59 | default: 60 | break; 61 | } 62 | break; 63 | } 64 | } 65 | 66 | /** 67 | * Activate watchers that track configuration changes for the given workspace 68 | * folder, or undefined if the workspace is top-level. 69 | */ 70 | export async function activate({ 71 | workspaceFolder, 72 | settings, 73 | paths, 74 | }: { 75 | workspaceFolder?: Optional; 76 | settings?: Optional; 77 | paths?: Optional; 78 | }): Promise { 79 | // Flag that controls whether a restart event was issued. This is used to 80 | // prevent multiple simultaneous restarts caused by, for example, multiple 81 | // watchers being triggered at once. 82 | let restartIssued = false; 83 | const promptRestartOnce = (promptMessage: string) => { 84 | if (restartIssued) { 85 | return; 86 | } 87 | restartIssued = true; 88 | promptRestart('onSettingsChanged', promptMessage); 89 | }; 90 | 91 | const disposables = new DisposableContext(); 92 | // When a configuration change happens, check to see if we should restart. 93 | disposables.pushSubscription( 94 | vscode.workspace.onDidChangeConfiguration((event) => { 95 | for (const setting of settings || []) { 96 | const expandedSetting = `mojo.${setting}`; 97 | if (event.affectsConfiguration(expandedSetting, workspaceFolder)) { 98 | promptRestartOnce( 99 | `setting '${ 100 | expandedSetting 101 | }' has changed. Do you want to reload the server?`, 102 | ); 103 | } 104 | } 105 | }), 106 | ); 107 | 108 | // Setup watchers for the provided paths. 109 | const fileWatcherConfig = { 110 | disableGlobbing: true, 111 | followSymlinks: true, 112 | ignoreInitial: true, 113 | awaitWriteFinish: true, 114 | }; 115 | for (const serverPath of paths || []) { 116 | // If the path actually exists, track it in case it changes. 117 | const fileWatcher = chokidar.watch(serverPath, fileWatcherConfig); 118 | fileWatcher.on('all', (event, _filename, _details) => { 119 | if (event != 'unlink') { 120 | promptRestartOnce( 121 | 'mojo language server file has changed. Do you want to reload the server?', 122 | ); 123 | } 124 | }); 125 | disposables.pushSubscription( 126 | new vscode.Disposable(() => { 127 | fileWatcher.close(); 128 | }), 129 | ); 130 | } 131 | return disposables; 132 | } 133 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Build and deploy extension 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | publish: 7 | description: Whether to publish to marketplaces. 8 | type: boolean 9 | default: true 10 | 11 | bump_version: 12 | description: Whether to increment the version number before publishing. 13 | type: boolean 14 | default: true 15 | 16 | new_version: 17 | description: The new version, passed to the `npm version` command. Has no effect if bump_version is off. 18 | type: choice 19 | required: true 20 | options: 21 | - major 22 | - minor 23 | - patch 24 | 25 | pre_release: 26 | description: Whether to release as a pre-release version. 27 | type: boolean 28 | default: false 29 | 30 | create_release: 31 | description: Whether to create a GitHub release. 32 | type: boolean 33 | default: false 34 | 35 | concurrency: 36 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 37 | cancel-in-progress: true 38 | 39 | jobs: 40 | version: 41 | name: Increment version 42 | runs-on: ubuntu-latest 43 | 44 | permissions: 45 | contents: write 46 | 47 | defaults: 48 | run: 49 | shell: bash 50 | 51 | outputs: 52 | version: ${{ steps.bump.outputs.version }} 53 | commit_sha: ${{ steps.get_sha.outputs.sha }} 54 | 55 | steps: 56 | - name: Setup Node 57 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a 58 | with: 59 | node-version: 20 60 | 61 | - name: Checkout repository 62 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 63 | 64 | - name: Configure Git 65 | run: | 66 | git config --global user.name 'Release Bot' 67 | git config --global user.email 'modularbot@modular.com' 68 | 69 | - name: Bump version 70 | if: ${{ inputs.bump_version }} 71 | run: npm version ${{ inputs.new_version }} 72 | 73 | - name: Push version bump 74 | if: ${{ inputs.bump_version }} 75 | run: git push 76 | 77 | - name: Extract package version 78 | id: bump 79 | run: | 80 | export VERSION=$(jq -r '.version' package.json) 81 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 82 | 83 | - name: Get new commit SHA 84 | id: get_sha 85 | run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 86 | 87 | build: 88 | needs: [version] 89 | uses: ./.github/workflows/build.yaml 90 | with: 91 | ref: ${{ needs.version.outputs.commit_sha || github.sha }} 92 | 93 | publish: 94 | name: Publish extension 95 | runs-on: ubuntu-latest 96 | needs: [version, build] 97 | 98 | defaults: 99 | run: 100 | shell: bash 101 | 102 | steps: 103 | - name: Checkout repository 104 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 105 | with: 106 | ref: ${{ needs.version.outputs.commit_sha || github.sha }} 107 | 108 | - uses: ./.github/actions/setup 109 | 110 | - name: Download built VSIX 111 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 112 | id: download 113 | with: 114 | name: ${{ needs.build.outputs.artifact-id }} 115 | 116 | - name: Enable pre-release builds 117 | if: ${{ inputs.pre_release }} 118 | run: | 119 | echo "PUBLISH_FLAGS=--pre-release" >> "$GITHUB_ENV" 120 | 121 | - name: Upload extension to VS Code marketplace 122 | if: ${{ inputs.publish }} 123 | env: 124 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 125 | run: npx vsce publish $PUBLISH_FLAGS --packagePath "${{ steps.download.outputs.download-path }}/vscode-mojo.vsix" --skip-duplicate 126 | 127 | - name: Upload extension to Open-VSX marketplace 128 | if: ${{ inputs.publish }} 129 | env: 130 | OVSX_PAT: ${{ secrets.OVSX_PAT }} 131 | run: npx ovsx publish $PUBLISH_FLAGS --packagePath "${{ steps.download.outputs.download-path }}/vscode-mojo.vsix" --skip-duplicate 132 | 133 | - name: Create release 134 | if: ${{ inputs.create_release }} 135 | uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 136 | with: 137 | draft: true 138 | tag_name: v${{ needs.version.output.version }} 139 | release_name: v${{ needs.version.output.version }} 140 | token: ${{ github.token }} 141 | files: ${{ steps.download.outputs.download-path }} 142 | generate_release_notes: true 143 | -------------------------------------------------------------------------------- /extension/debug/attachQuickPick.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as vscode from 'vscode'; 15 | 16 | import { ProcessDescriptor, psList } from '../external/psList'; 17 | import { Optional } from '../types'; 18 | 19 | class RefreshButton implements vscode.QuickInputButton { 20 | get iconPath(): vscode.ThemeIcon { 21 | return new vscode.ThemeIcon('extensions-refresh'); 22 | } 23 | 24 | get tooltip(): string { 25 | return 'Refresh process list'; 26 | } 27 | } 28 | 29 | interface ProcessItem extends vscode.QuickPickItem { 30 | id: number; 31 | } 32 | 33 | async function getProcessItems( 34 | context: vscode.ExtensionContext, 35 | ): Promise { 36 | const processes: ProcessDescriptor[] = await psList(context); 37 | processes.filter((p) => p.pid !== undefined); 38 | 39 | processes.sort((a, b) => { 40 | if (a.name === undefined) { 41 | if (b.name === undefined) { 42 | return 0; 43 | } 44 | return 1; 45 | } 46 | if (b.name === undefined) { 47 | return -1; 48 | } 49 | const aLower: string = a.name.toLowerCase(); 50 | const bLower: string = b.name.toLowerCase(); 51 | if (aLower === bLower) { 52 | return 0; 53 | } 54 | return aLower < bLower ? -1 : 1; 55 | }); 56 | return processes.map((process): ProcessItem => { 57 | return { 58 | label: process.name, 59 | description: `${process.pid}`, 60 | detail: process.cmd, 61 | id: process.pid, 62 | }; 63 | }); 64 | } 65 | 66 | /** 67 | * Show a QuickPick that selects a process to attach. 68 | * 69 | * @param context 70 | * @param debugConfig The debug config this action originates from. Its name is 71 | * used as cache key for persisting the filter the user used to find the 72 | * process to attach. 73 | * @returns The pid of the selected process as string. If the user cancelled, an 74 | * exception is thrown. 75 | */ 76 | async function showQuickPick( 77 | extensionContext: vscode.ExtensionContext, 78 | debugConfig: any, 79 | ): Promise> { 80 | const processItems: ProcessItem[] = await getProcessItems(extensionContext); 81 | const memento = extensionContext.workspaceState; 82 | const filterMementoKey = 'searchProgramToAttach' + debugConfig.name; 83 | const previousFilter = memento.get(filterMementoKey); 84 | 85 | return new Promise>((resolve, reject) => { 86 | const quickPick: vscode.QuickPick = 87 | vscode.window.createQuickPick(); 88 | quickPick.value = previousFilter || ''; 89 | quickPick.title = 'Attach to process'; 90 | quickPick.canSelectMany = false; 91 | quickPick.matchOnDescription = true; 92 | quickPick.matchOnDetail = true; 93 | quickPick.placeholder = 'Select the process to attach to'; 94 | quickPick.buttons = [new RefreshButton()]; 95 | quickPick.items = processItems; 96 | let textFilter = ''; 97 | const disposables: vscode.Disposable[] = []; 98 | 99 | quickPick.onDidTriggerButton( 100 | async () => { 101 | quickPick.items = await getProcessItems(extensionContext); 102 | }, 103 | undefined, 104 | disposables, 105 | ); 106 | 107 | quickPick.onDidChangeValue((e: string) => { 108 | textFilter = e; 109 | }); 110 | 111 | quickPick.onDidAccept( 112 | () => { 113 | if (quickPick.selectedItems.length !== 1) { 114 | reject(new Error('Process not selected.')); 115 | } 116 | 117 | const selectedId: string = `${quickPick.selectedItems[0].id}`; 118 | 119 | disposables.forEach((item) => item.dispose()); 120 | quickPick.dispose(); 121 | memento.update(filterMementoKey, textFilter); 122 | 123 | resolve(selectedId); 124 | }, 125 | undefined, 126 | disposables, 127 | ); 128 | 129 | quickPick.onDidHide( 130 | () => { 131 | disposables.forEach((item) => item.dispose()); 132 | quickPick.dispose(); 133 | 134 | resolve(undefined); 135 | }, 136 | undefined, 137 | disposables, 138 | ); 139 | 140 | quickPick.show(); 141 | }); 142 | } 143 | 144 | export function activatePickProcessToAttachCommand( 145 | extensionContext: vscode.ExtensionContext, 146 | ): vscode.Disposable { 147 | return vscode.commands.registerCommand( 148 | 'mojo.pickProcessToAttach', 149 | async (debugConfig: any) => showQuickPick(extensionContext, debugConfig), 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /lsp-proxy/src/streams.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import { ChildProcess } from 'child_process'; 15 | 16 | import { ExitStatus, JSONObject, Optional } from './types'; 17 | 18 | /** 19 | * A stream reader that reports whenever a line ending with `\n` is found. 20 | */ 21 | export class LineSeparatedStream { 22 | private enabled = true; 23 | 24 | constructor( 25 | rawStream: NodeJS.ReadableStream, 26 | onLine: (line: string) => void, 27 | ) { 28 | let buffer = ''; 29 | rawStream.on('data', (chunk: string) => { 30 | if (!this.enabled) { 31 | return; 32 | } 33 | 34 | buffer += chunk; 35 | 36 | let newLinePos = -1; 37 | while ((newLinePos = buffer.indexOf('\n')) !== -1) { 38 | const line = buffer.substring(0, newLinePos); 39 | buffer = buffer.substring(newLinePos + 1); 40 | onLine(line); 41 | } 42 | }); 43 | } 44 | 45 | public dispose() { 46 | this.enabled = false; 47 | } 48 | } 49 | 50 | /** 51 | * A stream reader based on the JSON-RPC protocol that reports whenever a 52 | * notification or the response to a request is found. 53 | */ 54 | export class JSONRPCStream { 55 | static protocolHeader = 'Content-Length: '; 56 | static protocolLineSeparator = '\r\n\r\n'; 57 | private buffer = ''; 58 | private enabled = true; 59 | 60 | constructor( 61 | rawStream: NodeJS.ReadableStream, 62 | onResponse: (response: JSONObject) => void, 63 | onNotification: (notification: JSONObject) => void, 64 | onOutgoingRequest: (request: JSONObject) => void, 65 | ) { 66 | rawStream.on('data', (chunk: string) => { 67 | if (!this.enabled) { 68 | return; 69 | } 70 | 71 | this.buffer += chunk; 72 | 73 | let packet: Optional; 74 | while ((packet = this.tryProcessPacket()) != undefined) { 75 | if ('id' in packet) { 76 | // Differentiate between a response to a client request or a request from the server to the client. 77 | if ('method' in packet) { 78 | onOutgoingRequest(packet); 79 | } else { 80 | onResponse(packet); 81 | } 82 | } else { 83 | onNotification(packet); 84 | } 85 | } 86 | return true; 87 | }); 88 | } 89 | 90 | /** 91 | * Tries to read a packet from the buffer and update that buffer if found. 92 | */ 93 | private tryProcessPacket(): Optional { 94 | // We process first the protocol header. 95 | if (!this.buffer.startsWith(JSONRPCStream.protocolHeader)) { 96 | return undefined; 97 | } 98 | // Then we parse the content length. 99 | let index = JSONRPCStream.protocolHeader.length; 100 | let contentLength = 0; 101 | for (; index < this.buffer.length; index++) { 102 | const c = this.buffer[index]; 103 | 104 | if (c < '0' || c > '9') { 105 | break; 106 | } 107 | contentLength = contentLength * 10 + parseInt(c); 108 | } 109 | // Then we parse the line separator. 110 | if ( 111 | !this.buffer 112 | .substring(index) 113 | .startsWith(JSONRPCStream.protocolLineSeparator) 114 | ) { 115 | return undefined; 116 | } 117 | 118 | // Then we extract the contents of the packet. 119 | const contentBegPos = index + JSONRPCStream.protocolLineSeparator.length; 120 | const contentBytes = Buffer.from(this.buffer.substring(contentBegPos)); 121 | 122 | if (contentBytes.length < contentLength) { 123 | return undefined; 124 | } 125 | const contents = contentBytes.subarray(0, contentLength).toString(); 126 | 127 | // We update the buffer to point past this packet. 128 | this.buffer = this.buffer.substring(contentBegPos + contents.length); 129 | return JSON.parse(contents); 130 | } 131 | 132 | public dispose() { 133 | this.enabled = false; 134 | } 135 | } 136 | 137 | /** 138 | * A stream reader that reports whenever a given process exists. Its underlying 139 | * callback will be invoked at most once. 140 | */ 141 | export class ProcessExitStream { 142 | private enabled = true; 143 | 144 | constructor(process: ChildProcess, onExit: (status: ExitStatus) => void) { 145 | process.on('exit', (code: number | null, signal: NodeJS.Signals | null) => { 146 | if (!this.enabled) { 147 | return; 148 | } 149 | onExit({ code, signal }); 150 | }); 151 | } 152 | 153 | public dispose() { 154 | this.enabled = false; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /extension/logging.ts: -------------------------------------------------------------------------------- 1 | // This file is a modified copy from 2 | // https://github.com/prettier/prettier-vscode/blob/main/src/logger.ts 3 | // which has MIT license. 4 | 5 | import * as vscode from 'vscode'; 6 | import { window } from 'vscode'; 7 | 8 | import { DisposableContext } from './utils/disposableContext'; 9 | 10 | export enum LogLevel { 11 | Trace = 0, 12 | Debug = 1, 13 | Info = 2, 14 | Warn = 3, 15 | Error = 4, 16 | None = 5, 17 | } 18 | 19 | const logLevelToString = (level: LogLevel) => { 20 | switch (level) { 21 | case LogLevel.Trace: 22 | return 'TRACE'; 23 | case LogLevel.Debug: 24 | return 'DEBUG'; 25 | case LogLevel.Info: 26 | return 'INFO'; 27 | case LogLevel.Warn: 28 | return 'WARN'; 29 | case LogLevel.Error: 30 | return 'ERROR'; 31 | case LogLevel.None: 32 | return 'NONE'; 33 | } 34 | }; 35 | 36 | export class LogChannel { 37 | readonly outputChannel: vscode.OutputChannel; 38 | private logLevel: LogLevel = LogLevel.Info; 39 | public logCallback?: (level: string, message: string) => void; 40 | 41 | constructor(outputChannelName: string) { 42 | this.outputChannel = window.createOutputChannel(outputChannelName); 43 | } 44 | 45 | public setOutputLevel(logLevel: LogLevel) { 46 | this.logLevel = logLevel; 47 | } 48 | 49 | /** 50 | * Append messages to the output channel and format it with a title 51 | * 52 | * @param message The message to append to the output channel 53 | */ 54 | public trace(message: string, data?: unknown): void { 55 | this.log(LogLevel.Trace, message); 56 | if (data) { 57 | this.log(LogLevel.Trace, data); 58 | } 59 | } 60 | 61 | /** 62 | * Append messages to the output channel and format it with a title 63 | * 64 | * @param message The message to append to the output channel 65 | */ 66 | public debug(message: string, data?: unknown): void { 67 | this.log(LogLevel.Debug, message); 68 | if (data) { 69 | this.log(LogLevel.Debug, data); 70 | } 71 | } 72 | 73 | /** 74 | * Append messages to the output channel and format it with a title 75 | * 76 | * @param message The message to append to the output channel 77 | */ 78 | public info(message: string, data?: unknown): void { 79 | this.log(LogLevel.Info, message); 80 | if (data) { 81 | this.log(LogLevel.Info, data); 82 | } 83 | } 84 | 85 | /** 86 | * Append messages to the output channel and format it with a title 87 | * 88 | * @param message The message to append to the output channel 89 | */ 90 | public warn(message: string, data?: unknown): void { 91 | this.log(LogLevel.Warn, message); 92 | if (data) { 93 | this.log(LogLevel.Warn, data); 94 | } 95 | } 96 | 97 | public error(message: string, error?: unknown) { 98 | this.log(LogLevel.Error, message); 99 | if (typeof error === 'string') { 100 | // Errors as a string usually only happen with plugins that don't return 101 | // the expected error. 102 | this.log(LogLevel.Error, error); 103 | } else if (error instanceof Error) { 104 | if (error?.message) { 105 | this.log(LogLevel.Error, error.message); 106 | } 107 | if (error?.stack) { 108 | this.log(LogLevel.Error, error.stack); 109 | } 110 | } else if (error) { 111 | this.log(LogLevel.Error, error); 112 | } 113 | } 114 | 115 | public show() { 116 | this.outputChannel.show(); 117 | } 118 | 119 | /** 120 | * Append messages to the output channel and format it with a title 121 | * 122 | * @param message The message to append to the output channel 123 | */ 124 | private log(logLevel: LogLevel, message: unknown): void { 125 | if (this.logLevel > logLevel) { 126 | return; 127 | } 128 | 129 | if (typeof message !== 'string') { 130 | message = JSON.stringify(message, null, 2); 131 | } 132 | 133 | const title = new Date().toLocaleTimeString(); 134 | this.outputChannel.appendLine( 135 | `["${logLevelToString(logLevel)}" - ${title}] ${message}`, 136 | ); 137 | 138 | if (this.logCallback) { 139 | // tsc doesn't understand that `message` is guaranteed to be a string at this point. 140 | this.logCallback(logLevelToString(logLevel), message as string); 141 | } 142 | } 143 | 144 | public dispose(): void { 145 | this.outputChannel.dispose(); 146 | } 147 | } 148 | 149 | export class Logger extends DisposableContext { 150 | public main: LogChannel; 151 | public lsp: LogChannel; 152 | 153 | constructor(initialLevel: LogLevel) { 154 | super(); 155 | 156 | this.main = new LogChannel('Mojo'); 157 | this.lsp = new LogChannel('Mojo Language Server'); 158 | 159 | this.main.setOutputLevel(initialLevel); 160 | this.lsp.setOutputLevel(initialLevel); 161 | 162 | this.pushSubscription(this.main); 163 | this.pushSubscription(this.lsp); 164 | } 165 | 166 | /** 167 | * Logs a TRACE message to the main log channel. 168 | */ 169 | public trace(message: string, data?: unknown) { 170 | this.main.trace(message, data); 171 | } 172 | 173 | /** 174 | * Logs a DEBUG message to the main log channel. 175 | */ 176 | public debug(message: string, data?: unknown) { 177 | this.main.debug(message, data); 178 | } 179 | 180 | /** 181 | * Logs an INFO message to the main log channel. 182 | */ 183 | public info(message: string, data?: unknown) { 184 | this.main.info(message, data); 185 | } 186 | 187 | /** 188 | * Logs a WARN message to the main log channel. 189 | */ 190 | public warn(message: string, data?: unknown) { 191 | this.main.warn(message, data); 192 | } 193 | 194 | /** 195 | * Logs an ERROR message to the main log channel. 196 | */ 197 | public error(message: string, data?: unknown) { 198 | this.main.error(message, data); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /extension/decorations.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as vscode from 'vscode'; 15 | 16 | import { DisposableContext } from './utils/disposableContext'; 17 | 18 | /** 19 | * MojoDecoratorContext is responsible for decorating mojo documents with 20 | * additional information. 21 | */ 22 | export class MojoDecoratorManager extends DisposableContext { 23 | private docStringDecorationType: vscode.TextEditorDecorationType; 24 | 25 | constructor() { 26 | super(); 27 | 28 | // Create a decoration type for doc strings. The decoration adds a `>` 29 | // before the doc string, to help visually distinguish it from the rest of 30 | // the code. This effectively models an inlay hint, but we use a decoration 31 | // type as decorations get refreshed much faster. 32 | this.docStringDecorationType = vscode.window.createTextEditorDecorationType( 33 | { 34 | after: { 35 | contentText: '>', 36 | color: { id: 'editorInlayHint.foreground' }, 37 | backgroundColor: { id: 'editorInlayHint.background' }, 38 | 39 | // Add a little padding to the right of the inlay hint. 40 | margin: '0em 0.2em 0em 0em', 41 | }, 42 | // Hide the decoration, we only care about the "after" content. 43 | opacity: '0', 44 | }, 45 | ); 46 | this.pushSubscription(this.docStringDecorationType); 47 | 48 | this.pushSubscription( 49 | vscode.workspace.onDidOpenTextDocument((event) => { 50 | this.decorateDocument(event); 51 | }), 52 | ); 53 | this.pushSubscription( 54 | vscode.workspace.onDidChangeTextDocument((event) => { 55 | this.decorateDocument(event.document); 56 | }), 57 | ); 58 | this.pushSubscription( 59 | vscode.workspace.onDidOpenNotebookDocument((event) => { 60 | this.decorateNotebookDocument(event); 61 | }), 62 | ); 63 | this.pushSubscription( 64 | vscode.workspace.onDidChangeNotebookDocument((event) => { 65 | this.decorateNotebookDocument(event.notebook); 66 | }), 67 | ); 68 | this.pushSubscription( 69 | vscode.window.onDidChangeVisibleTextEditors((editors) => { 70 | editors.forEach((editor) => this.decorate(editor)); 71 | }), 72 | ); 73 | this.pushSubscription( 74 | vscode.window.onDidChangeVisibleNotebookEditors((editors) => { 75 | editors.forEach((editor) => 76 | this.decorateNotebookDocument(editor.notebook), 77 | ); 78 | }), 79 | ); 80 | 81 | // Process any existing documents. 82 | 83 | // Process any existing documents. 84 | for (const textDoc of vscode.workspace.textDocuments) { 85 | this.decorateDocument(textDoc); 86 | } 87 | } 88 | 89 | private decorateDocument(document: vscode.TextDocument) { 90 | // Check if the document is a mojo document. 91 | 92 | // Check if the document is a mojo document. 93 | if ( 94 | !(document.languageId === 'mojo' || document.languageId === 'markdown') 95 | ) { 96 | return; 97 | } 98 | // Check if this is one of the visible editors. 99 | // Check if this is one of the visible editors. 100 | vscode.window.visibleTextEditors.forEach((editor) => { 101 | if (editor.document === document) { 102 | this.decorate(editor); 103 | } 104 | }); 105 | } 106 | 107 | private decorateNotebookDocument(notebook: vscode.NotebookDocument) { 108 | vscode.window.visibleNotebookEditors.forEach((editor) => { 109 | if (editor.notebook !== notebook) { 110 | return; 111 | } 112 | 113 | // Decorate any mojo cells in the notebook. 114 | 115 | // Decorate any mojo cells in the notebook. 116 | 117 | // Decorate any mojo cells in the notebook. 118 | for (const cell of notebook.getCells()) { 119 | this.decorateDocument(cell.document); 120 | } 121 | }); 122 | } 123 | 124 | /** 125 | * Generate decorations for the given text document. This includes decorations 126 | * for doc string code blocks, etc. 127 | */ 128 | private decorate(editor: vscode.TextEditor) { 129 | const text = editor.document.getText(); 130 | const splitLines = text.split('\n'); 131 | const docDecorations: vscode.DecorationOptions[] = []; 132 | 133 | // Generate decorations for code blocks in the document. This helps 134 | // visually distinguish code blocks from the rest of the document. 135 | const startRegEx = /^ *`{3,}mojo$/g; 136 | const endRegEx = /^ *`{3,}$/g; 137 | let numCurrentCodeBlocks = 0; 138 | let prevNumDecorations = 0; 139 | for (let line = 0, lineE = splitLines.length; line != lineE; ++line) { 140 | // Check for the start of a new codeblock. 141 | const currentLine = splitLines[line]; 142 | const match = startRegEx.test(currentLine); 143 | if (match) { 144 | if (numCurrentCodeBlocks++ === 0) { 145 | prevNumDecorations = docDecorations.length; 146 | } 147 | continue; 148 | } 149 | if (numCurrentCodeBlocks) { 150 | // Check for the end of a codeblock. 151 | if (endRegEx.test(currentLine)) { 152 | --numCurrentCodeBlocks; 153 | continue; 154 | } 155 | 156 | // Add a decoration for this code block. 157 | const pos = new vscode.Position(line, 0); 158 | docDecorations.push({ range: new vscode.Range(pos, pos) }); 159 | } 160 | } 161 | 162 | // If we have a partial code block, remove the decorations. 163 | 164 | // If we have a partial code block, remove the decorations. 165 | if (numCurrentCodeBlocks) { 166 | docDecorations.splice(prevNumDecorations); 167 | } 168 | 169 | editor.setDecorations(this.docStringDecorationType, docDecorations); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /extension/extension.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as vscode from 'vscode'; 15 | 16 | import { Logger, LogLevel } from './logging'; 17 | import { MojoLSPManager } from './lsp/lsp'; 18 | import * as configWatcher from './utils/configWatcher'; 19 | import { DisposableContext } from './utils/disposableContext'; 20 | import { registerFormatter } from './formatter'; 21 | import { activateRunCommands } from './commands/run'; 22 | import { MojoDebugManager } from './debug/debug'; 23 | import { MojoDecoratorManager } from './decorations'; 24 | import { RpcServer } from './server/RpcServer'; 25 | import { Mutex } from 'async-mutex'; 26 | import { TelemetryReporter } from './telemetry'; 27 | import { PythonEnvironmentManager } from './pyenv'; 28 | 29 | /** 30 | * This class provides an entry point for the Mojo extension, managing the 31 | * extension's state and disposal. 32 | */ 33 | export class MojoExtension extends DisposableContext { 34 | public logger: Logger; 35 | public readonly extensionContext: vscode.ExtensionContext; 36 | public lspManager?: MojoLSPManager; 37 | public pyenvManager?: PythonEnvironmentManager; 38 | private activateMutex = new Mutex(); 39 | private reporter: TelemetryReporter; 40 | 41 | constructor(context: vscode.ExtensionContext, logger: Logger) { 42 | super(); 43 | this.extensionContext = context; 44 | this.logger = logger; 45 | // NOTE: The telemetry connection string comes from the Azure Application Insights dashboard. 46 | this.reporter = new TelemetryReporter( 47 | context.extension.packageJSON.telemetryConnectionString, 48 | ); 49 | this.pushSubscription(this.reporter); 50 | 51 | // Disable telemetry for development and test environments. 52 | this.reporter.enabled = 53 | context.extensionMode == vscode.ExtensionMode.Production; 54 | } 55 | 56 | async activate(reloading: boolean): Promise { 57 | return await this.activateMutex.runExclusive(async () => { 58 | if (reloading) { 59 | this.dispose(); 60 | } 61 | 62 | this.logger.info(` 63 | ============================= 64 | Activating the Mojo Extension 65 | ============================= 66 | `); 67 | 68 | this.pyenvManager = new PythonEnvironmentManager( 69 | this.logger, 70 | this.reporter, 71 | ); 72 | this.pushSubscription(this.pyenvManager); 73 | await this.pyenvManager.init(); 74 | 75 | this.pushSubscription( 76 | await configWatcher.activate({ 77 | settings: ['SDK.additionalSDKs'], 78 | }), 79 | ); 80 | 81 | this.pushSubscription( 82 | vscode.commands.registerCommand('mojo.extension.restart', async () => { 83 | // Dispose and reactivate the context. 84 | await this.activate(/*reloading=*/ true); 85 | }), 86 | ); 87 | 88 | // Initialize the formatter. 89 | this.pushSubscription(registerFormatter(this.pyenvManager, this.logger)); 90 | 91 | // Initialize the debugger support. 92 | this.pushSubscription(new MojoDebugManager(this, this.pyenvManager)); 93 | 94 | // Initialize the execution commands. 95 | this.pushSubscription( 96 | activateRunCommands(this.pyenvManager, this.extensionContext), 97 | ); 98 | 99 | // Initialize the decorations. 100 | this.pushSubscription(new MojoDecoratorManager()); 101 | 102 | // Initialize the LSPs 103 | this.lspManager = new MojoLSPManager( 104 | this.pyenvManager, 105 | this.extensionContext, 106 | this.logger, 107 | this.reporter, 108 | ); 109 | await this.lspManager.activate(); 110 | this.pushSubscription(this.lspManager); 111 | 112 | this.logger.info('MojoContext activated.'); 113 | this.pushSubscription( 114 | new vscode.Disposable(() => { 115 | logger.info('Disposing MOJOContext.'); 116 | }), 117 | ); 118 | 119 | // Initialize the RPC server 120 | const rpcServer = new RpcServer(this.logger); 121 | this.logger.info('Starting RPC server'); 122 | this.pushSubscription(rpcServer); 123 | rpcServer.listen(); 124 | this.logger.info('Mojo extension initialized.'); 125 | return this; 126 | }); 127 | } 128 | 129 | override dispose() { 130 | this.logger.info('Disposing the extension.'); 131 | super.dispose(); 132 | } 133 | } 134 | 135 | export let extension: MojoExtension; 136 | let logger: Logger; 137 | let logHook: (level: string, message: string) => void; 138 | 139 | /** 140 | * This method is called when the extension is activated. See the 141 | * `activationEvents` in the package.json file for the current events that 142 | * activate this extension. 143 | */ 144 | export async function activate( 145 | context: vscode.ExtensionContext, 146 | ): Promise { 147 | logger = new Logger( 148 | context.extensionMode === vscode.ExtensionMode.Production 149 | ? LogLevel.Info 150 | : LogLevel.Debug, 151 | ); 152 | 153 | if (logHook) { 154 | logger.main.logCallback = logHook; 155 | logger.lsp.logCallback = logHook; 156 | } 157 | 158 | extension = new MojoExtension(context, logger); 159 | return extension.activate(/*reloading=*/ false); 160 | } 161 | 162 | /** 163 | * This method is called with VS Code deactivates this extension because of 164 | * an upgrade, a window reload, the editor is shutting down, or the user 165 | * disabled the extension manually. 166 | */ 167 | export function deactivate() { 168 | logger.info('Deactivating the extension.'); 169 | extension.dispose(); 170 | logger.info('Extension deactivated.'); 171 | logger.dispose(); 172 | } 173 | 174 | export function setLogHook(hook: (level: string, message: string) => void) { 175 | logHook = hook; 176 | if (logger) { 177 | logger.main.logCallback = hook; 178 | logger.lsp.logCallback = hook; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /extension/external/psList.ts: -------------------------------------------------------------------------------- 1 | // This file has been copied from https://github.com/sindresorhus/ps-list 2 | // using commit 6dbe8d6. 3 | // We have to copy paste it instead of adding it as a regular dependency 4 | // because the npm package `ps-list` is an ES module, which is not 5 | // compatible with Electron, the runtime for VS Code, which means that 6 | // we can't import it once our extension is loaded as a vsix package. 7 | 8 | // The only modification to this file is changing the line 9 | // const __dirname = path.dirname(fileURLToPath(import.meta.url)); 10 | // to using vscode.ExtensionContext.extensionPath to find the current 11 | // dirname. 12 | 13 | import * as childProcess from 'child_process'; 14 | import * as path from 'path'; 15 | import * as process from 'process'; 16 | import { promisify } from 'util'; 17 | import * as vscode from 'vscode'; 18 | 19 | const TEN_MEGABYTES = 1000 * 1000 * 10; 20 | const execFile = promisify(childProcess.execFile); 21 | 22 | const windows = async (context: vscode.ExtensionContext) => { 23 | // Source: https://github.com/MarkTiedemann/fastlist 24 | let binary; 25 | switch (process.arch) { 26 | case 'x64': 27 | binary = 'fastlist-0.3.0-x64.exe'; 28 | break; 29 | case 'ia32': 30 | binary = 'fastlist-0.3.0-x86.exe'; 31 | break; 32 | default: 33 | throw new Error(`Unsupported architecture: ${process.arch}`); 34 | } 35 | 36 | const binaryPath = path.join(context.extensionPath, 'bin', binary); 37 | const { stdout } = await execFile(binaryPath, { 38 | maxBuffer: TEN_MEGABYTES, 39 | windowsHide: true, 40 | }); 41 | 42 | return stdout 43 | .trim() 44 | .split('\r\n') 45 | .map((line) => line.split('\t')) 46 | .map(([pid, ppid, name]) => ({ 47 | pid: Number.parseInt(pid, 10), 48 | ppid: Number.parseInt(ppid, 10), 49 | name, 50 | })); 51 | }; 52 | 53 | const nonWindowsMultipleCalls = async (options: any = {}) => { 54 | const flags = (options.all === false ? '' : 'a') + 'wwxo'; 55 | const returnValue: { [index: string]: any } = {}; 56 | 57 | await Promise.all( 58 | ['comm', 'args', 'ppid', 'uid', '%cpu', '%mem'].map(async (cmd) => { 59 | const { stdout } = await execFile('ps', [flags, `pid,${cmd}`], { 60 | maxBuffer: TEN_MEGABYTES, 61 | }); 62 | 63 | for (let line of stdout.trim().split('\n').slice(1)) { 64 | line = line.trim(); 65 | const [pid] = line.split(' ', 1); 66 | const value = line.slice(pid.length + 1).trim(); 67 | 68 | if (returnValue[pid] === undefined) { 69 | returnValue[pid] = {}; 70 | } 71 | 72 | returnValue[pid][cmd] = value; 73 | } 74 | }), 75 | ); 76 | 77 | // Filter out inconsistencies as there might be race 78 | // issues due to differences in `ps` between the spawns 79 | return Object.entries(returnValue) 80 | .filter( 81 | ([, value]: any) => 82 | value.comm && 83 | value.args && 84 | value.ppid && 85 | value.uid && 86 | value['%cpu'] && 87 | value['%mem'], 88 | ) 89 | .map(([key, value]: any) => ({ 90 | pid: Number.parseInt(key, 10), 91 | name: path.basename(value.comm), 92 | cmd: value.args, 93 | ppid: Number.parseInt(value.ppid, 10), 94 | uid: Number.parseInt(value.uid, 10), 95 | cpu: Number.parseFloat(value['%cpu']), 96 | memory: Number.parseFloat(value['%mem']), 97 | })); 98 | }; 99 | 100 | const ERROR_MESSAGE_PARSING_FAILED = 'ps output parsing failed'; 101 | 102 | const psOutputRegex = 103 | /^[ \t]*(?\d+)[ \t]+(?\d+)[ \t]+(?[-\d]+)[ \t]+(?\d+\.\d+)[ \t]+(?\d+\.\d+)[ \t]+(?.*)?/; 104 | 105 | const nonWindowsCall = async (options: any = {}) => { 106 | const flags = options.all === false ? 'wwxo' : 'awwxo'; 107 | 108 | const psPromises = [ 109 | execFile('ps', [flags, 'pid,ppid,uid,%cpu,%mem,comm'], { 110 | maxBuffer: TEN_MEGABYTES, 111 | }), 112 | execFile('ps', [flags, 'pid,args'], { maxBuffer: TEN_MEGABYTES }), 113 | ]; 114 | 115 | const [psLines, psArgsLines] = (await Promise.all(psPromises)).map( 116 | ({ stdout }) => stdout.trim().split('\n'), 117 | ); 118 | 119 | const psPids = new Set(psPromises.map((promise) => promise.child.pid)); 120 | 121 | psLines.shift(); 122 | psArgsLines.shift(); 123 | 124 | const processCmds: { [index: string]: any } = {}; 125 | for (const line of psArgsLines) { 126 | const [pid, cmds]: any = line.trim().split(' '); 127 | processCmds[pid] = cmds.join(' '); 128 | } 129 | 130 | const processes = psLines 131 | .map((line) => { 132 | const match = psOutputRegex.exec(line); 133 | 134 | if (match === null) { 135 | throw new Error(ERROR_MESSAGE_PARSING_FAILED); 136 | } 137 | 138 | const { pid, ppid, uid, cpu, memory, comm }: any = match.groups; 139 | 140 | const processInfo = { 141 | pid: Number.parseInt(pid, 10), 142 | ppid: Number.parseInt(ppid, 10), 143 | uid: Number.parseInt(uid, 10), 144 | cpu: Number.parseFloat(cpu), 145 | memory: Number.parseFloat(memory), 146 | name: path.basename(comm), 147 | cmd: processCmds[pid], 148 | }; 149 | 150 | return processInfo; 151 | }) 152 | .filter((processInfo) => !psPids.has(processInfo.pid)); 153 | 154 | return processes; 155 | }; 156 | 157 | const nonWindows = async (options = {}) => { 158 | try { 159 | return await nonWindowsCall(options); 160 | } catch { 161 | // If the error is not a parsing error, it should manifest itself in 162 | // multicall version too. 163 | return nonWindowsMultipleCalls(options); 164 | } 165 | }; 166 | 167 | export interface Options { 168 | /** 169 | Include other users' processes as well as your own. 170 | 171 | On Windows this has no effect and will always be the users' own processes. 172 | 173 | @default true 174 | */ 175 | readonly all?: boolean; 176 | } 177 | export interface ProcessDescriptor { 178 | readonly pid: number; 179 | readonly name: string; 180 | readonly ppid: number; 181 | 182 | /** 183 | Not supported on Windows. 184 | */ 185 | readonly cmd?: string; 186 | 187 | /** 188 | Not supported on Windows. 189 | */ 190 | readonly cpu?: number; 191 | 192 | /** 193 | Not supported on Windows. 194 | */ 195 | readonly memory?: number; 196 | 197 | /** 198 | Not supported on Windows. 199 | */ 200 | readonly uid?: number; 201 | } 202 | 203 | export function psList( 204 | context: vscode.ExtensionContext, 205 | options?: Options, 206 | ): Promise { 207 | return process.platform === 'win32' ? windows(context) : nonWindows(options); 208 | } 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mojo🔥 extension for Visual Studio Code 2 | 3 | [![Build extension](https://github.com/modular/mojo-vscode/actions/workflows/build.yaml/badge.svg)](https://github.com/modular/mojo-vscode/actions/workflows/build.yaml) 4 | 5 | This VS Code extension from the Modular team adds support for the 6 | [Mojo programming language](https://www.modular.com/mojo). 7 | 8 | ## Features 9 | 10 | - Syntax highlighting for `.mojo` and `.🔥` files 11 | - Code completion 12 | - Code diagnostics and quick fixes 13 | - Full LSP experience for doc string code blocks 14 | - Go to symbol 15 | - API docs on hover 16 | - Code formatting 17 | - Run Mojo file 18 | 19 | ## Get started 20 | 21 | 1. [Install Mojo](https://docs.modular.com/mojo/manual/install). 22 | 2. [Install the Mojo VS Code extension](https://marketplace.visualstudio.com/items?itemName=modular-mojotools.vscode-mojo). 23 | 3. Open any `.mojo` or `.🔥` file and start coding. 24 | 25 | ### Mojo SDK resolution 26 | 27 | The extension relies on the Python extension for locating your Python 28 | environment. In some cases, this appears to default to your globally-installed 29 | environment, even when a virtual environment exists. 30 | 31 | If the Mojo extension cannot find your SDK installation, try invoking the 32 | `Python: Select Interpreter` command and selecting your virtual 33 | environment. 34 | 35 | ## Debugger 36 | 37 | A fully featured LLDB debugger is included with Mojo. You can press the down 38 | arrow next to the `▶️` button in the top right of a Mojo file, and select 39 | `Debug Mojo File`: 40 | 41 | ![debugging](https://github.com/modular/mojo/assets/77730378/45c547c3-8f08-4f8c-85a4-1254d12a09f5) 42 | 43 | The default key is `F5`, and you can rebind the related hotkeys in Preferences: 44 | Open Keyboard Shortcuts > `Debug: Start Debugging`. 45 | 46 | For details, see the [Mojo debugging 47 | guide](https://docs.modular.com/mojo/tools/debugging). 48 | 49 | ## Code completion 50 | 51 | To trigger a completion press `ctrl + space`, pressing `ctrl + space` again will 52 | bring up doc hints: 53 | 54 | ![completion](https://github.com/modular/mojo/assets/77730378/51af7c47-8c39-449b-a759-8351c543208a) 55 | 56 | Rebind the hotkey in Preferences: Open Keyboard Shortcuts > `Trigger Suggest` 57 | 58 | ## Hover and doc hints 59 | 60 | Hover over a symbol with your cursor for doc hints. The default hotkey 61 | to trigger it in macOS is `⌘ + k`, `⌘ + i` or `ctrl + k`, `ctrl + i` in Linux 62 | and Windows: 63 | 64 | ![hover](https://github.com/modular/mojo/assets/77730378/59881310-d2ec-481f-975a-d69d5e6c7ae3) 65 | 66 | Rebind the hotkey in Preferences: Open Keyboard Shortcuts > 67 | `Show or Focus Hover` 68 | 69 | ## Signature help 70 | 71 | Mojo provides function overloading, so you need a way to scroll through the 72 | multiple signatures available. You can bring this up with the hotkey 73 | `⌘ + shift + space` in macOS or `ctrl + shift + space` in Linux or Windows. 74 | 75 | ![signature-help](https://github.com/modular/mojo/assets/77730378/3994ab6d-ae4b-43af-9ddf-0d979c51330f) 76 | 77 | Rebind related hotkeys in Preferences: Open Keyboard Shortcuts > 78 | `Trigger Parameter Hints` 79 | 80 | ## Code diagnostics 81 | 82 | Code diagnostics are indicated with an underline on the code and details appear 83 | when you hover. You can also see them in the `PROBLEMS` tab and use 84 | `Go to Next Problem in Files` to quickly cycle through them: 85 | 86 | ![diagnostics2](https://github.com/modular/mojo/assets/77730378/b9d4c570-62da-4e82-981d-6d95ea8f34a2) 87 | 88 | Rebind related hotkeys in Preferences: Open Keyboard Shortcuts > 89 | `Go to Next Problem...` 90 | 91 | **Tip:** Also try the `Error Lens` extension (not associated with Modular), 92 | which will display the first line of the diagnostic inline, making it easier 93 | to quickly fix problems. 94 | 95 | ## Doc string code blocks 96 | 97 | Unique to Mojo, you get a full LSP experience for code blocks inside doc 98 | strings, with all the features mentioned here including completions and 99 | diagnostics: 100 | 101 | ![doc-lsp](https://github.com/modular/mojo/assets/77730378/c2d73fd0-66de-44e7-8125-511bf0237396) 102 | 103 | ## Go to symbol 104 | 105 | You can quickly jump to a symbol in the file with `⌘ + shift + o` in macOS or 106 | `ctrl + shift + o` in Linux and Windows. 107 | 108 | ![go-to-symbol](https://github.com/modular/mojo/assets/77730378/1972e611-4a01-4a7f-945d-a3b5f10034a9) 109 | 110 | This also enables the outline view in the explorer window. 111 | 112 | Rebind the hotkey in Preferences: Open Keyboard Shortcuts > 113 | `Go to Symbol in Editor` 114 | 115 | ## Quick fix 116 | 117 | If there is an available quick fix with the code diagnostic, click 118 | the lightbulb icon or use the default hotkey `ctrl + .` for a list of options: 119 | 120 | ![quick-fix](https://github.com/modular/mojo/assets/77730378/b9bb1122-9fdc-4fbc-b3a8-28a54cd78704) 121 | 122 | Rebind the hotkey in Preferences: Open Keyboard Shortcuts > 123 | `Quick Fix...` 124 | 125 | ## Run Mojo file 126 | 127 | The extension provides a set of actions on the top-right of a Mojo file to run 128 | the active file, which by default are under a small `▶️` button up the 129 | top-right of the editor: 130 | 131 | ![run-file](https://github.com/modular/mojo/assets/77730378/22ef37cf-154a-430b-9ef3-427dbab411fc) 132 | 133 | These actions are also available in the command palette and under the `Mojo` 134 | submenu in the File Explorer when right-clicking on Mojo files: 135 | 136 | ![right-click-menu](https://github.com/modular/mojo/assets/77730378/b267a44c-fa2c-425d-bada-7360cd338351) 137 | 138 | You may bind hotkeys to any of the actions listed here. For example, to bind a 139 | hotkey for the "Run Mojo File" action, open preferences, then select 140 | `Keyboard Shortcuts > Mojo: Run Mojo File`. 141 | 142 | ### `Run Mojo File` 143 | 144 | This executes the current Mojo file in a terminal that is reused by other 145 | invocations of this same action, even if they run a different file. 146 | 147 | ### `Run Mojo File in Dedicated Terminal` 148 | 149 | This executes the current Mojo file in a dedicated terminal that is reused only 150 | by subsequent runs of this very same file. 151 | 152 | ## Code formatting 153 | 154 | From the command palette run `Format Document` or tick the setting 155 | `Format on Save`: 156 | 157 | ![format](https://github.com/modular/mojo/assets/77730378/4e0e22c4-0216-41d7-b5a5-7f48a018fd81) 158 | 159 | ## Restarting Mojo extension 160 | 161 | The extension may crash and produce incorrect results periodically, to fix this 162 | from the command palette search for `Mojo: Restart the extension` 163 | 164 | ![restart](https://github.com/modular/mojo/assets/77730378/c65bf84b-5c9b-4151-8176-2b098533dbe3) 165 | 166 | Bind a hotkey in Preferences: Open Keyboard Shortcuts > 167 | `Mojo: Restart the extension` 168 | -------------------------------------------------------------------------------- /lsp-proxy/src/MojoLSPServer.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import { ChildProcess, spawn } from 'child_process'; 15 | import { firstValueFrom, Subject } from 'rxjs'; 16 | 17 | import { DisposableCallback, DisposableContext } from './DisposableContext'; 18 | import { 19 | JSONRPCStream, 20 | LineSeparatedStream, 21 | ProcessExitStream, 22 | } from './streams'; 23 | import { 24 | ExitStatus, 25 | InitializationOptions, 26 | JSONObject, 27 | Optional, 28 | RequestId, 29 | RequestParams, 30 | } from './types'; 31 | 32 | const protocolHeader = 'Content-Length: '; 33 | const protocolLineSeparator = '\r\n\r\n'; 34 | 35 | type PendingRequest = { 36 | params: RequestParams; 37 | responseStream: Subject; 38 | }; 39 | 40 | /** 41 | * This class manages an instance of the mojo-lsp-server process, as well as 42 | * supporting utilities for sending requests and notifications. 43 | */ 44 | export class MojoLSPServer extends DisposableContext { 45 | private serverProcess: ChildProcess; 46 | private lastSentRequestId: RequestId = -1; 47 | private pendingRequests = new Map(); 48 | /** 49 | * @param initializationOptions The options needed to spawn the 50 | * mojo-lsp-server. 51 | * @param logger The callback used to log messages to the LSP output channel. 52 | * This logger is expected to append a newline after each invocation. 53 | * @param onExit A callback invoked whenever the server exits. 54 | */ 55 | constructor({ 56 | initializationOptions, 57 | logger, 58 | onExit, 59 | onNotification, 60 | onOutgoingRequest, 61 | }: { 62 | initializationOptions: InitializationOptions; 63 | logger: (message: string) => void; 64 | onExit: (status: ExitStatus) => void; 65 | onNotification: (method: string, params: JSONObject) => void; 66 | onOutgoingRequest: (id: any, method: string, params: JSONObject) => void; 67 | }) { 68 | super(); 69 | 70 | this.serverProcess = spawn( 71 | initializationOptions.serverPath, 72 | initializationOptions.serverArgs, 73 | { 74 | env: initializationOptions.serverEnv, 75 | }, 76 | ); 77 | this.pushSubscription( 78 | new LineSeparatedStream(this.serverProcess.stderr!, (line: string) => 79 | logger(line), 80 | ), 81 | ); 82 | this.pushSubscription( 83 | new JSONRPCStream( 84 | this.serverProcess.stdout!, 85 | (response: JSONObject) => 86 | this.pendingRequests.get(response.id)!.responseStream.next(response), 87 | (notification: JSONObject) => 88 | onNotification(notification.method, notification.params), 89 | (request: JSONObject) => 90 | onOutgoingRequest(request.id, request.method, request.params), 91 | ), 92 | ); 93 | this.pushSubscription(new ProcessExitStream(this.serverProcess, onExit)); 94 | this.pushSubscription( 95 | new DisposableCallback(() => { 96 | // We kill the server process after all listeners have been disposed, to 97 | // guarantee that no listener is invoked when the process dies. 98 | try { 99 | this.serverProcess.kill(); 100 | } catch (e) { 101 | console.error(e); 102 | } 103 | }), 104 | ); 105 | } 106 | 107 | /** 108 | * Send a request to the server given its params and a method name that 109 | * follows the LSP protocol. 110 | * @returns a promise with the payload that gets resolved when the request is 111 | * responded. 112 | */ 113 | public async sendRequest( 114 | params: RequestParams, 115 | method: string, 116 | ): Promise { 117 | const request = this.wrapRequest(params, method); 118 | const id = request.id; 119 | await this.sendPacket(request); 120 | 121 | const subject = new Subject(); 122 | this.pendingRequests.set(id, { params: params, responseStream: subject }); 123 | const result = (await firstValueFrom(subject)).result; 124 | this.pendingRequests.delete(id); 125 | return result; 126 | } 127 | 128 | /** 129 | * Send a notification to the server given its params and a method name that 130 | * follows the LSP protocol. 131 | */ 132 | public sendNotification(params: T, method: string): void { 133 | const notification = this.wrapNotification(params, method); 134 | this.sendPacket(notification); 135 | } 136 | 137 | /** 138 | * Send a response to a server -> client request, given a response body and a 139 | * request ID. 140 | */ 141 | public sendResponse(id: any, result: unknown): void { 142 | const response = this.wrapResponse(id, result); 143 | this.sendPacket(response); 144 | } 145 | 146 | public sendError(id: any, error: unknown): void { 147 | const response = this.wrapResponse(id, undefined, error); 148 | this.sendPacket(response); 149 | } 150 | 151 | /** 152 | * @returns A new incremental request Id that can be used for sending 153 | * requests. 154 | */ 155 | private getNewRequestId(): number { 156 | this.lastSentRequestId++; 157 | return this.lastSentRequestId; 158 | } 159 | 160 | /** 161 | * Sends some arbitrary data that is sent to the server using the JSON RPC 162 | * protocol. 163 | */ 164 | private async sendPacket(packet: T): Promise { 165 | const payload = Buffer.from(JSON.stringify(packet)); 166 | return new Promise((resolve, _reject) => { 167 | return this.serverProcess.stdin?.write( 168 | `${protocolHeader}${payload.length}${protocolLineSeparator}${payload}`, 169 | () => resolve(), 170 | ); 171 | }); 172 | } 173 | 174 | /** 175 | * Wraps some params and method within a new object that is ready to be sent 176 | * to the server as a request. 177 | */ 178 | private wrapRequest(params: T, method: string): any { 179 | return { 180 | id: this.getNewRequestId(), 181 | jsonrpc: '2.0', 182 | method: method, 183 | params: params, 184 | }; 185 | } 186 | 187 | /** 188 | * Wraps some params and method within a new object that is ready to be sent 189 | * to the server as a notification. 190 | */ 191 | private wrapNotification(params: T, method: string): any { 192 | return { 193 | jsonrpc: '2.0', 194 | method: method, 195 | params: params, 196 | }; 197 | } 198 | 199 | /** 200 | * Wraps an ID and params as a response object, 201 | */ 202 | private wrapResponse(id: any, result?: unknown, error?: unknown): JSONObject { 203 | return { 204 | jsonrpc: '2.0', 205 | id, 206 | result, 207 | error, 208 | }; 209 | } 210 | 211 | /** 212 | * @returns the params of the oldest pending request. 213 | */ 214 | public getOldestPendingRequest(): Optional { 215 | for (const params of this.pendingRequests.values()) { 216 | return params.params; 217 | } 218 | return undefined; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /extension/server/RpcServer.ts: -------------------------------------------------------------------------------- 1 | // The following code is a modification of 2 | // https://github.com/vadimcn/codelldb/blob/master/extension/externalLaunch.ts, 3 | // which has MIT license. 4 | 5 | import * as net from 'net'; 6 | import * as vscode from 'vscode'; 7 | import { debug, DebugConfiguration } from 'vscode'; 8 | import { Logger } from '../logging'; 9 | import { checkNsightInstall } from '../utils/checkNsight'; 10 | import { DisposableContext } from '../utils/disposableContext'; 11 | import { Optional } from '../types'; 12 | 13 | type ResponseConnect = { 14 | kind: 'connect'; 15 | pid: number; 16 | lastTimeSeenActiveInSecs: number; 17 | name: Optional; 18 | }; 19 | 20 | type ResponseDebug = { 21 | kind: 'debug'; 22 | }; 23 | type Response = ResponseConnect | ResponseDebug; 24 | 25 | type RPCServerResponse = 26 | | ({ success: true } & Response) 27 | | { 28 | success: false; 29 | message?: string; 30 | kind?: string; 31 | }; 32 | 33 | type RequestConnect = { 34 | kind: 'connect'; 35 | }; 36 | type RequestDebug = { 37 | kind: 'debug'; 38 | debugConfiguration: DebugConfiguration; 39 | }; 40 | 41 | function instanceOfConnect(object: any): object is RequestConnect { 42 | return object.kind === 'connect'; 43 | } 44 | 45 | function instanceOfDebug(object: any): object is RequestDebug { 46 | return ( 47 | object.kind === 'debug' && typeof object.debugConfiguration === 'object' 48 | ); 49 | } 50 | 51 | const PORT_MIN = 12355; 52 | const PORT_MAX = 12364; // Inclusive 53 | 54 | /** 55 | * RPC Server. 56 | * 57 | * It listens for network messages dispatching actions on this extension. 58 | * Messages are JSON objects followed by a `\n----\n`. 59 | */ 60 | export class RpcServer extends DisposableContext { 61 | private server: net.Server; 62 | private port: number = PORT_MIN; 63 | private logger: Logger; 64 | private readonly protocolSeparator = '\n----\n'; 65 | private lastTimeActiveInMillis: Date = new Date(); 66 | 67 | constructor(logger: Logger) { 68 | super(); 69 | this.logger = logger; 70 | 71 | this.server = net.createServer({ allowHalfOpen: true }); 72 | const clients: net.Socket[] = []; 73 | this.server.on('error', (err) => this.onError(err)); 74 | this.server.on('connection', (socket) => { 75 | this.configureSocket(socket); 76 | clients.push(socket); 77 | socket.on('close', () => { 78 | clients.splice(clients.indexOf(socket), 1); 79 | }); 80 | }); 81 | 82 | this.pushSubscription( 83 | new vscode.Disposable(() => { 84 | for (const client of clients) { 85 | client.destroy(); 86 | } 87 | this.server.close(() => { 88 | this.logger.info('RPC server closed.'); 89 | this.server.unref(); 90 | }); 91 | }), 92 | ); 93 | this.pushSubscription( 94 | vscode.window.onDidChangeWindowState((e: vscode.WindowState) => { 95 | if (e.active) { 96 | this.lastTimeActiveInMillis = new Date(); 97 | } 98 | }), 99 | ); 100 | } 101 | 102 | private onError(err: Error): void { 103 | if (err.message.includes('EADDRINUSE') && this.port < PORT_MAX) { 104 | this.logger.info('Will try to start the RPC Server with a new port.'); 105 | this.port += 1; 106 | this.listen(); 107 | } else { 108 | this.logger.error( 109 | 'RPC Server error. You might need to restart VS Code to fix this issue.', 110 | err, 111 | ); 112 | } 113 | } 114 | 115 | // Launch a debug session. Throws if debug session initialization has error. 116 | private async handleDebugRequest(debugConfig: DebugConfiguration) { 117 | debugConfig.name = debugConfig.name || debugConfig.program; 118 | if (debugConfig.type === 'mojo-cuda-gdb') { 119 | const maybeErrorMessage = await checkNsightInstall(this.logger); 120 | if (maybeErrorMessage) { 121 | throw new Error(maybeErrorMessage); 122 | } 123 | } 124 | const success = await debug.startDebugging( 125 | /*workspaceFolder=*/ undefined, 126 | debugConfig, 127 | ); 128 | 129 | if (!success) { 130 | throw new Error('Unable to start the debug session'); 131 | } 132 | } 133 | 134 | private async dispatchRequest( 135 | socket: net.Socket, 136 | rawRequest: string, 137 | ): Promise { 138 | let request: Optional; 139 | try { 140 | const parsedRequest = JSON.parse(rawRequest); 141 | if (typeof parsedRequest === 'object') { 142 | request = parsedRequest; 143 | } 144 | } catch (err) { 145 | this.logger.info(`RPC Server request parsing error: ${err}`); 146 | } 147 | 148 | if (request === undefined) { 149 | const response: RPCServerResponse = { 150 | success: false, 151 | message: 'Malformed request. Not a JSON object.', 152 | }; 153 | socket.end(JSON.stringify(response) + this.protocolSeparator); 154 | return; 155 | } 156 | this.logger.info(`RPC Server request: ${JSON.stringify(request)}`); 157 | 158 | if (instanceOfConnect(request)) { 159 | let name = '[VSCode]'; 160 | const activeTab = vscode.window.tabGroups.activeTabGroup.activeTab?.label; 161 | if (activeTab !== undefined) { 162 | name += ` ${activeTab}`; 163 | } else { 164 | const ws = vscode.workspace.workspaceFolders?.at(0); 165 | if (ws !== undefined) { 166 | name += ` ${ws.name}`; 167 | } 168 | } 169 | 170 | const now = new Date().getTime(); 171 | const response: RPCServerResponse = { 172 | success: true, 173 | kind: 'connect', 174 | pid: process.pid, 175 | lastTimeSeenActiveInSecs: Math.floor( 176 | (now - this.lastTimeActiveInMillis.getTime()) / 1000, 177 | ), 178 | name, 179 | }; 180 | this.logger.info(`RPC Server response: ${JSON.stringify(response)}`); 181 | socket.write(JSON.stringify(response) + this.protocolSeparator); 182 | } else if (instanceOfDebug(request)) { 183 | const debugConfig: DebugConfiguration = request.debugConfiguration; 184 | try { 185 | await this.handleDebugRequest(debugConfig); 186 | const response: RPCServerResponse = { 187 | success: true, 188 | kind: 'debug', 189 | }; 190 | this.logger.info(`RPC Server response: ${JSON.stringify(response)}`); 191 | socket.write(JSON.stringify(response) + this.protocolSeparator); 192 | } catch (err) { 193 | const response: RPCServerResponse = { 194 | success: false, 195 | message: `${err}`, 196 | kind: 'debug', 197 | }; 198 | this.logger.info(`RPC Server response: ${JSON.stringify(response)}`); 199 | socket.write(JSON.stringify(response) + this.protocolSeparator); 200 | } 201 | } else { 202 | const response: RPCServerResponse = { 203 | success: false, 204 | message: 'Invalid request', 205 | }; 206 | this.logger.info(`RPC Server response: ${JSON.stringify(response)}`); 207 | socket.end(JSON.stringify(response) + this.protocolSeparator); 208 | } 209 | } 210 | 211 | private configureSocket(socket: net.Socket) { 212 | let buffer = ''; 213 | socket.on('data', async (chunk: any) => { 214 | buffer += chunk; 215 | while (buffer.includes(this.protocolSeparator)) { 216 | const pos = buffer.indexOf(this.protocolSeparator); 217 | const rawRequest = buffer.substring(0, pos); 218 | buffer = buffer.substring(pos + this.protocolSeparator.length); 219 | await this.dispatchRequest(socket, rawRequest); 220 | } 221 | }); 222 | socket.on('end', () => { 223 | socket.end(); 224 | }); 225 | } 226 | 227 | /** 228 | * Listens to messages using the provided network options. 229 | */ 230 | public async listen() { 231 | this.logger.info( 232 | `Attempting to create the RPC server with port ${this.port}`, 233 | ); 234 | 235 | return new Promise((resolve) => 236 | this.server.listen({ port: this.port, host: '127.0.0.1' }, () => 237 | resolve(this.server.address() || ''), 238 | ), 239 | ); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Modular contributor guide 2 | 3 | Thank you for your interest in contributing to this repository! 4 | 5 | This page explains the overall process to create a pull request (PR), from 6 | forking the repo all the way through review and final merge. 7 | 8 | ## Submitting bugs 9 | 10 | Reporting issues is a great way to contribute to the project. 11 | 12 | Before opening a new issue, take a moment to search through the already 13 | [submitted issues](https://github.com/modular/mojo-vscode/issues) to avoid creating 14 | duplicate issues for the maintainers to address. 15 | 16 | ### Writing high-quality bug descriptions 17 | 18 | Bugs with a reproducible test case and well-written descriptions will be 19 | considered a higher priority. 20 | 21 | We encourage you to provide as much information about the issue as practical. 22 | The more details you provide, the faster we can resolve the issue. The following 23 | is a template of the information that should accompany every submitted issue. 24 | 25 | #### Issue template 26 | 27 | - **Summary.** A descriptive summary of the issue. 28 | - **Description.** A detailed account of the bug, including what was expected 29 | and what occurred. 30 | - **Environment details.** 31 | - MAX or Mojo version (run `max --version` or `mojo --version`) 32 | - Operating system version 33 | - Hardware specifications 34 | - **Severity/frequency.** An assessment of the impact ranging from inconvenience 35 | to a blocker. 36 | 37 | ## Contributing changes 38 | 39 | Before you start your first pull request, please complete this checklist: 40 | 41 | - Read this entire contributor guide. 42 | - Read the [Code of Conduct](./CODE_OF_CONDUCT.md). 43 | 44 | ### Step 1: Evaluate and get buy-in on the change 45 | 46 | We want to be sure that you spend your time efficiently and prepare 47 | changes that aren’t controversial and get stuck in long rounds of reviews. So 48 | if the change is non-trivial, please submit an issue or write a proposal, as 49 | described in the corresponding sections. 50 | 51 | ### Step 2: Create a pull request 52 | 53 | If you're experienced with GitHub, here's the basic process: 54 | 55 | 1. Fork this repo. 56 | 57 | 2. Create a branch from `main`. 58 | 59 | 3. Create a PR into the `main` branch of this repo. 60 | 61 | 4. Skip to [Step 3: PR triage and review](#step-3-pr-triage-and-review). 62 | 63 | #### Pull request walkthrough 64 | 65 | For more specifics, here's a detailed walkthrough of the process to create a 66 | pull request: 67 | 68 | 1. Fork and clone this repo: 69 | 70 | Go to the [repo home](https://github.com/modular/mojo-vscode) and click 71 | the **Fork** button at the top. 72 | 73 | Your fork will be accessible at `https://github.com//mojo-vscode`. 74 | 75 | Clone your forked repo to your computer: 76 | 77 | ```bash 78 | git clone git@github.com:/modular.git 79 | cd modular 80 | ``` 81 | 82 | To clarify, you're working with three repo entities: 83 | 84 | - This repo (`https://github.com/modular/mojo-vscode`) is known as the upstream 85 | repo. In Git terminology, it's the _upstream remote_. 86 | - Your fork on GitHub is known as _origin_ (also remote). 87 | - Your local clone is stored on our computer. 88 | 89 | Because a fork can diverge from the upstream repo it was forked from, it is 90 | crucial to configure our local clone to track upstream changes: 91 | 92 | ```bash 93 | git remote add upstream git@github.com:modular/mojo-vscode.git 94 | ``` 95 | 96 | Then sync your fork to the latest code from upstream: 97 | 98 | ```bash 99 | git pull --rebase upstream 100 | ``` 101 | 102 | 2. Create a branch off `main` to work on your change: 103 | 104 | ```bash 105 | git checkout -b my-fix 106 | ``` 107 | 108 | Now start your work on the repo! If you're contributing to the Mojo 109 | standard library, see the [Mojo standard library developer 110 | guide](mojo/stdlib/docs/development.md). 111 | 112 | Although not necessary right now, you should periodically make sure you have 113 | the latest code, especially right before you create the pull request: 114 | 115 | ```bash 116 | git fetch upstream 117 | git rebase upstream/main 118 | ``` 119 | 120 | 3. Create a pull request: 121 | 122 | When you're code is ready, create a pull request into the `main` branch. 123 | 124 | First push the local changes to your origin on GitHub: 125 | 126 | ```bash 127 | git push -u origin my-fix 128 | ``` 129 | 130 | You'll see a link to create a PR: 131 | 132 | ```plaintext 133 | remote: Create a pull request for 'my-fix' on GitHub by visiting: 134 | remote: https://github.com/[your-username]/mojo-vscode/pull/new/my-fix 135 | ``` 136 | 137 | You can open that URL or visit your fork on GitHub and click **Contribute** to 138 | start a pull request. 139 | 140 | GitHub should automatically set the base repository to `modular/mojo-vscode` 141 | and the base (branch) to `main`. If not, you can select it from the drop-down. 142 | Then click **Create pull request**. 143 | 144 | Now fill out the pull request details in the GitHub UI: 145 | 146 | - Add a short commit title describing the change. 147 | - Add a detailed commit description that includes rationalization for the change 148 | and/or explanation of the problem that it solves, with a link to any relevant 149 | GitHub issues. 150 | 151 | Click **Create pull request**. 152 | 153 | ### Step 3: PR triage and review 154 | 155 | A Modular team member will take an initial look the the pull request and 156 | determine how to proceed. This may include: 157 | 158 | - **Leaving the PR as-is** (e.g. if it's a draft). 159 | - **Reviewing the PR directly**, especially if the changes are straightforward. 160 | - **Assigning the PR** to a subject-matter expert on the appropriate team 161 | (Libraries, Kernels, Documentation etc.) for deeper review. 162 | 163 | We aim to respond in a timely manner based on the time tables in the 164 | [guidelines for review time](#guidelines-for-review-time), below. 165 | 166 | ### Step 4: Review feedback and iteration 167 | 168 | All feedback intended for you will be posted directly on the **external** pull 169 | request. Internal discussions (e.g. security/privacy reviews or cross-team 170 | coordination) may happen privately but won't affect your ability to contribute. 171 | If we need changes from you, we'll leave clear comments with action items. 172 | 173 | Once everything is approved and CI checks pass, we'll take care of the final 174 | steps to get your PR merged. 175 | 176 | Merged changes will generally show up in the the next nightly build (or docs 177 | website), a day or two after it's merged. 178 | 179 | ## Guidelines for review time 180 | 181 | 1. Pull Request (PR) Review Timeline 182 | 183 | Initial Review: 184 | 185 | - Maintainers will provide an initial review or feedback within 3 weeks of 186 | the PR submission. At times, it may be significantly quicker, but it 187 | depends on a variety of factors. 188 | 189 | Subsequent Reviews: 190 | 191 | - Once a contributor addresses feedback, maintainers will review updates as 192 | soon as they can, typically within 5 business days. 193 | 194 | 1. Issue Triage Timeline 195 | 196 | New Issues: 197 | 198 | - Maintainers will label and acknowledge new issues within 10 days of the 199 | issue submission. 200 | 201 | 1. Proposals 202 | 203 | - Proposals take more time for the team to review, discuss, and make sure this 204 | is in line with the overall strategy and vision for the standard library. 205 | These will get discussed in the team's weekly design meetings internally and 206 | feedback will be communicated back on the relevant proposal. As a team, we'll 207 | ensure these get reviewed and discussed within 6 weeks of submission. 208 | 209 | ### Exceptions 210 | 211 | While we strive our best to adhere to these timelines, there may be occasional 212 | delays due any of the following: 213 | 214 | - High volume of contributions. 215 | - Maintainers' availability (e.g. holidays, team events). 216 | - Complex issues or PRs requiring extended discussion (these may get deferred to 217 | the team's weekly design discussion meetings). 218 | 219 | Note that just because a pull request has been reviewed does not necessarily 220 | mean it will be able to be merged internally immediately. This could be due to a 221 | variety of reasons, such as: 222 | 223 | - Mojo compiler bugs. These take time to find a minimal reproducer, file an 224 | issue with the compiler team, and then get prioritized and fixed. 225 | - Internal bugs that get exposed due to a changeset. 226 | - Massive refactorings due to an external changeset. These also take time to 227 | fix - remember, we have the largest Mojo codebase in the world internally. 228 | 229 | If delays occur, we'll provide status updates in the relevant thread (pull 230 | request or GitHub issue). Please bear with us as Mojo is an early language. 231 | We look forward to working together with you in making Mojo better for everyone! 232 | 233 | ### How you can help 234 | 235 | To ensure quicker reviews: 236 | 237 | - Ensure your PR is small and focused. 238 | - Write a good commit message/PR summary outlining the motivation and describing 239 | the changes. In the near future, we'll provide a pull request template to 240 | clarify this further. 241 | - Use descriptive titles and comments for clarity. 242 | - Code-review other contributor pull requests and help each other. 243 | 244 | ## 🙌 Thanks for contributing 245 | 246 | We deeply appreciate your interest in improving the Modular ecosystem. Whether 247 | you're fixing typos, improving docs, or contributing core library features, your 248 | input makes a difference. 249 | 250 | If you have questions or need help, feel free to: 251 | 252 | - Leave a comment on your pull request 253 | - Join our community [forum](https://forum.modular.com/) and post a question 254 | 255 | Let's build something great together! 256 | -------------------------------------------------------------------------------- /extension/commands/run.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import { quote, parse } from 'shell-quote'; 15 | import * as vscode from 'vscode'; 16 | import { DisposableContext } from '../utils/disposableContext'; 17 | import * as path from 'path'; 18 | import * as config from '../utils/config'; 19 | import { MojoDebugConfiguration } from '../debug/debug'; 20 | import md5 from 'md5'; 21 | import { Optional } from '../types'; 22 | import { PythonEnvironmentManager, SDK } from '../pyenv'; 23 | 24 | type FileArgs = { 25 | runArgs: string[]; 26 | buildArgs: string[]; 27 | }; 28 | 29 | /** 30 | * This class provides a manager for executing and debugging mojo files. 31 | */ 32 | class ExecutionManager extends DisposableContext { 33 | readonly envManager: PythonEnvironmentManager; 34 | private context: vscode.ExtensionContext; 35 | 36 | constructor( 37 | sdkManager: PythonEnvironmentManager, 38 | context: vscode.ExtensionContext, 39 | ) { 40 | super(); 41 | 42 | this.envManager = sdkManager; 43 | this.context = context; 44 | this.activateRunCommands(); 45 | } 46 | 47 | private getFileArgsKey(path: string): string { 48 | return `file.args.${path}`; 49 | } 50 | 51 | private getFileArgs(path: string): FileArgs { 52 | return this.context.globalState.get(this.getFileArgsKey(path), { 53 | runArgs: [], 54 | buildArgs: [], 55 | }); 56 | } 57 | 58 | private getBuildArgs(path: string): string[] { 59 | return this.getFileArgs(path).buildArgs; 60 | } 61 | 62 | private async setBuildArgs(path: string, args: string): Promise { 63 | const fileArgs = this.getFileArgs(path); 64 | fileArgs.buildArgs = parse(args).filter( 65 | (x): x is string => typeof x === 'string', 66 | ); 67 | return this.context.globalState.update(this.getFileArgsKey(path), fileArgs); 68 | } 69 | 70 | private getRunArgs(path: string): string[] { 71 | return this.getFileArgs(path).runArgs; 72 | } 73 | 74 | private async setRunArgs(path: string, args: string): Promise { 75 | const fileArgs = this.getFileArgs(path); 76 | fileArgs.runArgs = parse(args).filter( 77 | (x): x is string => typeof x === 'string', 78 | ); 79 | return this.context.globalState.update(this.getFileArgsKey(path), fileArgs); 80 | } 81 | 82 | /** 83 | * Activate the run commands, used for executing and debugging mojo files. 84 | */ 85 | activateRunCommands() { 86 | const cmd = 'mojo.file.run'; 87 | this.pushSubscription( 88 | vscode.commands.registerCommand(cmd, (file?: vscode.Uri) => { 89 | this.executeFileInTerminal(file); 90 | return true; 91 | }), 92 | ); 93 | 94 | for (const cmd of ['mojo.file.debug', 'mojo.file.debug-in-terminal']) { 95 | this.pushSubscription( 96 | vscode.commands.registerCommand(cmd, (file: vscode.Uri) => { 97 | this.debugFile( 98 | file, 99 | /*runInTerminal=*/ cmd === 'mojo.file.debug-in-terminal', 100 | ); 101 | return true; 102 | }), 103 | ); 104 | } 105 | this.pushSubscription( 106 | vscode.commands.registerCommand( 107 | 'mojo.file.set-args', 108 | async (file: vscode.Uri) => { 109 | const setBuildArgs = 'Set Build Arguments'; 110 | const setRunArgs = 'Set Run Arguments'; 111 | const option = await vscode.window.showQuickPick( 112 | [setBuildArgs, setRunArgs], 113 | { 114 | title: 'Select the arguments you want to configure', 115 | placeHolder: 116 | 'This will affect `Run Mojo File`, `Debug Mojo File` and similar actions.', 117 | }, 118 | ); 119 | 120 | if (option === setBuildArgs) { 121 | const buildArgs = quote(this.getBuildArgs(file.fsPath)); 122 | 123 | const newValue = await vscode.window.showInputBox({ 124 | placeHolder: 'Enter the arguments as if within a shell.', 125 | title: 'Enter the build arguments for the compiler', 126 | value: buildArgs.length === 0 ? undefined : buildArgs, 127 | }); 128 | if (newValue !== undefined) { 129 | await this.setBuildArgs(file.fsPath, newValue); 130 | } 131 | } else if (option === setRunArgs) { 132 | const runArgs = quote(this.getRunArgs(file.fsPath)); 133 | 134 | const newValue = await vscode.window.showInputBox({ 135 | placeHolder: 'Enter the arguments as if within a shell.', 136 | title: 'Enter the run arguments for the final executable', 137 | value: runArgs.length === 0 ? undefined : runArgs, 138 | }); 139 | if (newValue !== undefined) { 140 | await this.setRunArgs(file.fsPath, newValue); 141 | } 142 | } 143 | return true; 144 | }, 145 | ), 146 | ); 147 | } 148 | 149 | /** 150 | * Execute the current file in a terminal. 151 | * 152 | * @param options Options to consider when executing the file. 153 | */ 154 | async executeFileInTerminal(file: Optional) { 155 | const doc = await this.getDocumentToExecute(file); 156 | 157 | if (!doc) { 158 | return; 159 | } 160 | 161 | // Find the config for processing this file. 162 | const sdk = await this.envManager.getActiveSDK(); 163 | 164 | if (!sdk) { 165 | return; 166 | } 167 | 168 | // Execute the file. 169 | const terminal = this.getTerminalForFile(doc, sdk); 170 | terminal.show(); 171 | terminal.sendText( 172 | quote([ 173 | sdk.mojoPath, 174 | 'run', 175 | ...this.getBuildArgs(doc.fileName), 176 | doc.fileName, 177 | ...this.getRunArgs(doc.fileName), 178 | ]), 179 | ); 180 | 181 | if (this.shouldTerminalFocusOnStart(doc.uri)) { 182 | vscode.commands.executeCommand('workbench.action.terminal.focus'); 183 | 184 | // Sometimes VSCode will focus on the terminal as a side-effect of `terminal.show()`, 185 | // in which case we need to indicate it to switch back the focus to the previous 186 | // focus group. 187 | } else { 188 | vscode.commands.executeCommand('workbench.action.focusActiveEditorGroup'); 189 | } 190 | } 191 | 192 | /** 193 | * Debug the current file. 194 | * 195 | * @param runInTerminal If true, then a target is launched in a new 196 | * terminal, and therefore its stdin and stdout are not managed by the 197 | * Debug Console. 198 | */ 199 | async debugFile(file: Optional, runInTerminal: boolean) { 200 | const doc = await this.getDocumentToExecute(file); 201 | 202 | if (!doc) { 203 | return; 204 | } 205 | 206 | const debugConfig: MojoDebugConfiguration = { 207 | type: 'mojo-lldb', 208 | name: 'Mojo', 209 | request: 'launch', 210 | mojoFile: doc.fileName, 211 | runInTerminal: runInTerminal, 212 | buildArgs: this.getBuildArgs(doc.fileName), 213 | args: this.getRunArgs(doc.fileName), 214 | }; 215 | await vscode.debug.startDebugging( 216 | vscode.workspace.getWorkspaceFolder(doc.uri), 217 | debugConfig as vscode.DebugConfiguration, 218 | ); 219 | } 220 | 221 | /** 222 | * Get a terminal to use for the given file. 223 | */ 224 | getTerminalForFile(doc: vscode.TextDocument, sdk: SDK): vscode.Terminal { 225 | const fullId = `${doc.fileName} · ${sdk.mojoPath}`; 226 | // We have to keep the full terminal name short so that VS Code renders it nicely, 227 | // and we have to keep it unique among other files. 228 | const terminalName = `Mojo: ${path.basename(doc.fileName)} · ${md5(fullId).substring(0, 5)}`; 229 | 230 | // Look for an existing terminal. 231 | const terminal = vscode.window.terminals.find( 232 | (t) => t.name === terminalName, 233 | ); 234 | 235 | if (terminal) { 236 | return terminal; 237 | } 238 | 239 | // Build a new terminal. 240 | return vscode.window.createTerminal({ 241 | name: terminalName, 242 | env: sdk.getProcessEnv(), 243 | hideFromUser: true, 244 | }); 245 | } 246 | 247 | /** 248 | * Get the vscode.Document to execute, ensuring that it's saved if pending 249 | * changes exist. 250 | * 251 | * This method show a pop up in case of errors. 252 | * 253 | * @param file If provided, the document will point to this file, otherwise, 254 | * it will point to the currently active document. 255 | */ 256 | async getDocumentToExecute( 257 | file?: vscode.Uri, 258 | ): Promise> { 259 | const doc = 260 | file === undefined 261 | ? vscode.window.activeTextEditor?.document 262 | : await vscode.workspace.openTextDocument(file); 263 | if (!doc) { 264 | vscode.window.showErrorMessage( 265 | `Couldn't access the file '${file}' for execution.`, 266 | ); 267 | return undefined; 268 | } 269 | if (doc.isDirty && !(await doc.save())) { 270 | vscode.window.showErrorMessage( 271 | `Couldn't save file '${file}' before execution.`, 272 | ); 273 | return undefined; 274 | } 275 | return doc; 276 | } 277 | 278 | /** 279 | * Returns true if the terminal should be focused on start. 280 | */ 281 | private shouldTerminalFocusOnStart(uri: vscode.Uri): boolean { 282 | return config.get( 283 | 'run.focusOnTerminalAfterLaunch', 284 | vscode.workspace.getWorkspaceFolder(uri), 285 | false, 286 | ); 287 | } 288 | } 289 | 290 | /** 291 | * Activate the run commands, used for executing and debugging mojo files. 292 | * 293 | * @returns A disposable connected to the lifetime of the registered run 294 | * commands. 295 | */ 296 | export function activateRunCommands( 297 | envManager: PythonEnvironmentManager, 298 | context: vscode.ExtensionContext, 299 | ): vscode.Disposable { 300 | return new ExecutionManager(envManager, context); 301 | } 302 | -------------------------------------------------------------------------------- /extension/debug/inlineVariables.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as vscode from 'vscode'; 15 | 16 | import { MojoExtension } from '../extension'; 17 | import { DisposableContext } from '../utils/disposableContext'; 18 | 19 | import { DEBUG_TYPE } from './constants'; 20 | import { Optional } from '../types'; 21 | import { 22 | VariableEvaluateName, 23 | Variable, 24 | FrameId, 25 | RequestId, 26 | DAPScopesRequest, 27 | DAPVariablesRequest, 28 | DAPVariablesResponse, 29 | SessionId, 30 | } from './types'; 31 | 32 | /** 33 | * Variables grouped by evaluate name. Multiple entries per key represent 34 | * shadowed variables. 35 | */ 36 | type VariablesGroups = Map; 37 | 38 | /** 39 | * Class that tracks the local variables of every frame by inspecting the DAP 40 | * messages. 41 | * 42 | * The only interesting detail is that the "variables" request doesn't have a 43 | * `frameId`. Instead, this request is followed by the "scopes" request, which 44 | * does have a `frameId`, so we keep an eye on this successive pair of requests 45 | * to produce the appropriate mapping. 46 | */ 47 | export class LocalVariablesTracker implements vscode.DebugAdapterTracker { 48 | /** 49 | * The current `frameId` gotten from the last "scopes" request. 50 | */ 51 | private currentFrameId: FrameId = -1; 52 | /** 53 | * A mapping from frameId to a grouped list of variables. These groups 54 | * represent shadowed variables. 55 | */ 56 | public frameToVariables = new Map(); 57 | /** 58 | * A mapping that helps us identify which frameId corresponds to a given 59 | * variables request. 60 | */ 61 | public variablesRequestIdToFrameId = new Map(); 62 | /** 63 | * This is a hardcoded value in lldb-dap that represents the list of local 64 | * variables. 65 | */ 66 | private static LOCAL_SCOPE_ID = 1; 67 | public onFrameGotVariables = new vscode.EventEmitter< 68 | [FrameId, VariablesGroups] 69 | >(); 70 | 71 | async waitForFrameVariables(frameId: FrameId): Promise { 72 | const result = this.frameToVariables.get(frameId); 73 | 74 | if (result !== undefined) { 75 | return result; 76 | } 77 | 78 | return new Promise((resolve, _reject) => { 79 | this.onFrameGotVariables.event(([eventFrameId, variables]) => { 80 | if (eventFrameId === frameId) { 81 | resolve(variables); 82 | } 83 | }); 84 | }); 85 | } 86 | 87 | onWillReceiveMessage(message: { [key: string]: unknown }): void { 88 | if (message.command === 'scopes') { 89 | this.currentFrameId = (message as DAPScopesRequest).arguments.frameId; 90 | } else if (message.command === 'variables') { 91 | const request = message as DAPVariablesRequest; 92 | if ( 93 | request.arguments.variablesReference === 94 | LocalVariablesTracker.LOCAL_SCOPE_ID 95 | ) { 96 | this.variablesRequestIdToFrameId.set(request.seq, this.currentFrameId); 97 | } 98 | } 99 | } 100 | 101 | onDidSendMessage(message: { [key: string]: unknown }): void { 102 | if (message.event === 'stopped') { 103 | this.currentFrameId = -1; 104 | this.frameToVariables.clear(); 105 | this.variablesRequestIdToFrameId.clear(); 106 | } 107 | 108 | if (message.command === 'variables') { 109 | const response = message as DAPVariablesResponse; 110 | const variablesMap: VariablesGroups = new Map(); 111 | 112 | for (const variable of response.body.variables) { 113 | if (!variablesMap.has(variable.evaluateName)) { 114 | variablesMap.set(variable.evaluateName, []); 115 | } 116 | variablesMap.get(variable.evaluateName)!.push(variable); 117 | } 118 | 119 | const frameId = this.variablesRequestIdToFrameId.get( 120 | response.request_seq, 121 | )!; 122 | this.frameToVariables.set(frameId, variablesMap); 123 | this.onFrameGotVariables.fire([frameId, variablesMap]); 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * Provides inline local variables during a debug session. 130 | */ 131 | export class InlineLocalVariablesProvider 132 | implements vscode.InlineValuesProvider 133 | { 134 | private localVariablesTrackers: Map; 135 | private extension: MojoExtension; 136 | 137 | constructor( 138 | extension: MojoExtension, 139 | localVariablesTrackers: Map, 140 | ) { 141 | this.extension = extension; 142 | this.localVariablesTrackers = localVariablesTrackers; 143 | } 144 | 145 | /** 146 | * Create the inline text to show for the given variable. 147 | */ 148 | private createInlineVariableValue( 149 | line: number, 150 | column: number, 151 | variable: Variable, 152 | ): vscode.InlineValueText { 153 | const displayName = variable.evaluateName; 154 | const range = new vscode.Range( 155 | line, 156 | column, 157 | line, 158 | column + variable.evaluateName.length, 159 | ); 160 | // The value cannot be extremely long, so we cap it. 161 | const inlineVariableValueLengthCap = 50; 162 | const value = 163 | variable.value.length >= inlineVariableValueLengthCap 164 | ? variable.value.substring(0, inlineVariableValueLengthCap) + '...' 165 | : variable.value; 166 | return new vscode.InlineValueText(range, `${displayName} = ${value}`); 167 | } 168 | 169 | /** 170 | * Find the column in the document where the given variable is declared. 171 | * Currently DWARF doesn't have columns (#29230), so we have to look for the 172 | * declaration column using text search in the document. 173 | */ 174 | private findDeclColumn( 175 | document: vscode.TextDocument, 176 | line: number, 177 | variable: Variable, 178 | ): Optional { 179 | const text = document.lineAt(line).text; 180 | let index = -1; 181 | 182 | // This is used to verify that a candidate declaration for our variable 183 | // cannot be expanded into a larger variable name. 184 | const forbiddenBoundary = (char?: string) => 185 | char !== undefined && /^[a-zA-Z0-9_]$/.test(char); 186 | 187 | while (true) { 188 | index = text.indexOf(variable.evaluateName, index + 1); 189 | 190 | if (index === -1) { 191 | break; 192 | } 193 | 194 | const prev = text[index - 1]; 195 | const next = text[index + variable.evaluateName.length]; 196 | 197 | if (!forbiddenBoundary(prev) && !forbiddenBoundary(next)) { 198 | return index; 199 | } 200 | } 201 | 202 | return undefined; 203 | } 204 | 205 | /** 206 | * Create the list of inline values for a given variable using the LSP's index 207 | * of references. 208 | */ 209 | async getInlineValuesForVariable( 210 | document: vscode.TextDocument, 211 | stoppedLocation: vscode.Range, 212 | variable: Variable, 213 | ): Promise { 214 | const decl = variable.$__lldb_extensions.declaration; 215 | const error = variable.$__lldb_extensions.error || ''; 216 | const path = decl?.path || ''; 217 | 218 | if (decl?.line === undefined || path.length === 0 || error.length > 0) { 219 | return []; 220 | } 221 | const line = decl.line - 1; 222 | // If the decl line is where we are stopped or later, we don't inline the 223 | // variable to prevent printing dirty memory. 224 | 225 | // If the decl line is where we are stopped or later, we don't inline the 226 | // variable to prevent printing dirty memory. 227 | if (line >= stoppedLocation.start.line) { 228 | return []; 229 | } 230 | const column = this.findDeclColumn(document, line, variable); 231 | 232 | // If there's no column information, we can at least show the variable in 233 | // the decl line. 234 | if (column === undefined) { 235 | return [this.createInlineVariableValue(line, 0, variable)]; 236 | } 237 | 238 | const uri = vscode.Uri.file(path); 239 | const lspServer = this.extension.lspManager?.lspClient; 240 | 241 | if (lspServer === undefined) { 242 | return []; 243 | } 244 | 245 | const references: undefined | any[] = await lspServer.sendRequest( 246 | 'textDocument/references', 247 | { 248 | textDocument: { 249 | uri: uri.toString(), 250 | }, 251 | context: { includeDeclaration: true }, 252 | position: { 253 | line: line, 254 | character: column, 255 | }, 256 | }, 257 | ); 258 | return (references || []) 259 | .map((ref) => 260 | this.createInlineVariableValue( 261 | ref.range.start.line, 262 | ref.range.start.character, 263 | variable, 264 | ), 265 | ) 266 | .filter( 267 | // We only keep the references that are on the stop line or above. 268 | (inlineVar) => inlineVar.range.start.line <= stoppedLocation.start.line, 269 | ); 270 | } 271 | 272 | async provideInlineValues( 273 | document: vscode.TextDocument, 274 | _viewport: vscode.Range, 275 | context: vscode.InlineValueContext, 276 | ): Promise { 277 | const tracker = this.localVariablesTrackers.get( 278 | vscode.debug.activeDebugSession?.id || '', 279 | ); 280 | if (tracker === undefined) { 281 | // This could be a non-bug if there are two simultaneous debug sessions 282 | // with different debuggers. 283 | this.extension.logger?.error( 284 | `Couldn't find the local variable tracker for sessionId ${ 285 | vscode.debug.activeDebugSession?.id 286 | } and frameId ${context.frameId}.`, 287 | ); 288 | return []; 289 | } 290 | 291 | const variableGroups = await tracker.waitForFrameVariables(context.frameId); 292 | 293 | const allValues: vscode.InlineValue[] = []; 294 | for (const variables of variableGroups.values()) { 295 | for (const variable of variables) { 296 | allValues.push( 297 | ...(await this.getInlineValuesForVariable( 298 | document, 299 | context.stoppedLocation, 300 | variable, 301 | )), 302 | ); 303 | } 304 | } 305 | return allValues; 306 | } 307 | } 308 | 309 | export function initializeInlineLocalVariablesProvider( 310 | extension: MojoExtension, 311 | ): DisposableContext { 312 | const localVariablesTrackers: Map = 313 | new Map(); 314 | const disposables = new DisposableContext(); 315 | 316 | disposables.pushSubscription( 317 | vscode.debug.registerDebugAdapterTrackerFactory(DEBUG_TYPE, < 318 | vscode.DebugAdapterTrackerFactory 319 | >{ 320 | createDebugAdapterTracker( 321 | session: vscode.DebugSession, 322 | ): vscode.ProviderResult { 323 | const tracker = new LocalVariablesTracker(); 324 | localVariablesTrackers.set(session.id, tracker); 325 | return tracker; 326 | }, 327 | }), 328 | ); 329 | disposables.pushSubscription( 330 | vscode.debug.onDidTerminateDebugSession((session: vscode.DebugSession) => { 331 | localVariablesTrackers.delete(session.id); 332 | }), 333 | ); 334 | 335 | disposables.pushSubscription( 336 | vscode.languages.registerInlineValuesProvider( 337 | '*', 338 | new InlineLocalVariablesProvider(extension, localVariablesTrackers), 339 | ), 340 | ); 341 | return disposables; 342 | } 343 | -------------------------------------------------------------------------------- /extension/lsp/lsp.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as vscode from 'vscode'; 15 | import * as vscodelc from 'vscode-languageclient/node'; 16 | import { TransportKind } from 'vscode-languageclient/node'; 17 | 18 | import * as config from '../utils/config'; 19 | import { DisposableContext } from '../utils/disposableContext'; 20 | import { Subject } from 'rxjs'; 21 | import { Logger } from '../logging'; 22 | import { TelemetryReporter } from '../telemetry'; 23 | import { LSPRecorder } from './recorder'; 24 | import { Optional } from '../types'; 25 | import { PythonEnvironmentManager, SDK } from '../pyenv'; 26 | import path from 'path'; 27 | 28 | /** 29 | * This type represents the initialization options send by the extension to the 30 | * proxy. 31 | */ 32 | export interface InitializationOptions { 33 | /** 34 | * The path to `mojo-lsp-server`. 35 | */ 36 | serverPath: string; 37 | /** 38 | * The arguments to use when invoking `mojo-lsp-server`. 39 | */ 40 | serverArgs: string[]; 41 | /** 42 | * The environment to use when invoking `mojo-lsp-server`. 43 | */ 44 | serverEnv: { [env: string]: Optional }; 45 | } 46 | 47 | /** 48 | * This class manages the LSP clients. 49 | */ 50 | export class MojoLSPManager extends DisposableContext { 51 | private extensionContext: vscode.ExtensionContext; 52 | private envManager: PythonEnvironmentManager; 53 | public lspClient: Optional; 54 | public lspClientChanges = new Subject>(); 55 | private logger: Logger; 56 | private reporter: TelemetryReporter; 57 | private recorder: Optional; 58 | private statusBarItem: Optional; 59 | private attachDebugger: boolean = false; 60 | 61 | constructor( 62 | envManager: PythonEnvironmentManager, 63 | extensionContext: vscode.ExtensionContext, 64 | logger: Logger, 65 | reporter: TelemetryReporter, 66 | ) { 67 | super(); 68 | 69 | this.envManager = envManager; 70 | this.extensionContext = extensionContext; 71 | this.logger = logger; 72 | this.reporter = reporter; 73 | } 74 | 75 | async activate() { 76 | this.pushSubscription( 77 | vscode.commands.registerCommand('mojo.lsp.restart', async () => { 78 | // Wait for the language server to stop. This allows a graceful shutdown of the server instead of simply terminating the process, which is important for tracing. 79 | if (this.lspClient) { 80 | await this.lspClient.stop(); 81 | } 82 | 83 | this.dispose(); 84 | this.lspClient = undefined; 85 | await this.activate(); 86 | }), 87 | ); 88 | 89 | if ( 90 | this.extensionContext.extensionMode == vscode.ExtensionMode.Development || 91 | this.extensionContext.extensionMode == vscode.ExtensionMode.Test 92 | ) { 93 | this.pushSubscription( 94 | vscode.commands.registerCommand('mojo.lsp.debug', async () => { 95 | if (this.lspClient) { 96 | await this.lspClient.stop(); 97 | } 98 | 99 | this.attachDebugger = true; 100 | 101 | this.dispose(); 102 | this.lspClient = undefined; 103 | await this.activate(); 104 | }), 105 | ); 106 | 107 | this.pushSubscription( 108 | vscode.commands.registerTextEditorCommand( 109 | 'mojo.lsp.dumpParsedIR', 110 | async (textEditor) => { 111 | if (!this.lspClient) { 112 | return; 113 | } 114 | 115 | await this.lspClient.sendNotification('mojo/emitParsedIR', { 116 | uri: textEditor.document.uri.toString(), 117 | }); 118 | }, 119 | ), 120 | ); 121 | } 122 | 123 | this.statusBarItem = vscode.window.createStatusBarItem( 124 | 'lsp-recording-state', 125 | vscode.StatusBarAlignment.Right, 126 | ); 127 | this.statusBarItem.text = 'Mojo LSP $(record)'; 128 | this.statusBarItem.backgroundColor = new vscode.ThemeColor( 129 | 'statusBarItem.warningBackground', 130 | ); 131 | this.statusBarItem.command = 'mojo.lsp.stopRecord'; 132 | this.pushSubscription(this.statusBarItem); 133 | 134 | this.pushSubscription( 135 | vscode.commands.registerCommand('mojo.lsp.startRecord', async () => { 136 | if (this.recorder) { 137 | this.recorder.dispose(); 138 | } 139 | 140 | if ( 141 | !vscode.workspace.workspaceFolders || 142 | vscode.workspace.workspaceFolders.length == 0 143 | ) { 144 | return; 145 | } 146 | const workspaceFolder = vscode.workspace.workspaceFolders[0]; 147 | const recordPath = vscode.Uri.joinPath( 148 | workspaceFolder.uri, 149 | 'mojo-lsp-recording.jsonl', 150 | ); 151 | 152 | this.recorder = new LSPRecorder(recordPath.fsPath); 153 | this.pushSubscription(this.recorder); 154 | 155 | vscode.window 156 | .showInformationMessage( 157 | `Started recording language server session to ${recordPath}.`, 158 | 'Stop', 159 | 'Open', 160 | ) 161 | .then((action) => { 162 | switch (action) { 163 | case 'Open': 164 | return vscode.commands.executeCommand( 165 | 'vscode.open', 166 | recordPath, 167 | ); 168 | case 'Stop': 169 | return vscode.commands.executeCommand('mojo.lsp.stopRecord'); 170 | } 171 | }); 172 | 173 | this.statusBarItem!.tooltip = `Recording Mojo LSP session to ${recordPath}`; 174 | this.statusBarItem!.show(); 175 | }), 176 | ); 177 | 178 | this.pushSubscription( 179 | vscode.commands.registerCommand('mojo.lsp.stopRecord', async () => { 180 | if (!this.recorder) { 181 | return; 182 | } 183 | 184 | this.recorder!.dispose(); 185 | this.recorder = undefined; 186 | this.statusBarItem!.hide(); 187 | }), 188 | ); 189 | 190 | vscode.workspace.textDocuments.forEach((doc) => 191 | this.tryStartLanguageClient(doc), 192 | ); 193 | this.pushSubscription( 194 | vscode.workspace.onDidOpenTextDocument((doc) => 195 | this.tryStartLanguageClient(doc), 196 | ), 197 | ); 198 | 199 | this.pushSubscription( 200 | this.envManager.onEnvironmentChange(() => { 201 | this.logger.info('Restarting language server due to SDK change'); 202 | vscode.commands.executeCommand('mojo.lsp.restart'); 203 | }), 204 | ); 205 | } 206 | 207 | async tryStartLanguageClient(doc: vscode.TextDocument): Promise { 208 | if (doc.languageId !== 'mojo') { 209 | return; 210 | } 211 | 212 | const sdk = await this.envManager.getActiveSDK(); 213 | 214 | if (!sdk) { 215 | return; 216 | } 217 | 218 | if (this.lspClient !== undefined) { 219 | return; 220 | } 221 | 222 | const includeDirs = config.get( 223 | 'lsp.includeDirs', 224 | /*workspaceFolder=*/ undefined, 225 | [], 226 | ); 227 | const lspClient = this.activateLanguageClient(sdk, includeDirs); 228 | this.lspClient = lspClient; 229 | this.lspClientChanges.next(lspClient); 230 | this.pushSubscription( 231 | new vscode.Disposable(() => { 232 | lspClient.stop(); 233 | lspClient.dispose(); 234 | this.lspClientChanges.next(undefined); 235 | this.lspClientChanges.unsubscribe(); 236 | }), 237 | ); 238 | } 239 | 240 | /** 241 | * Create a new language server. 242 | */ 243 | activateLanguageClient( 244 | sdk: SDK, 245 | includeDirs: string[], 246 | ): vscodelc.LanguageClient { 247 | this.logger.lsp.info('Activating language client'); 248 | 249 | const serverArgs: string[] = []; 250 | 251 | for (const includeDir of includeDirs) { 252 | serverArgs.push('-I', includeDir); 253 | } 254 | 255 | if (this.attachDebugger) { 256 | serverArgs.push('--attach-debugger-on-startup'); 257 | } 258 | 259 | const initializationOptions: InitializationOptions = { 260 | serverArgs: serverArgs, 261 | serverEnv: sdk.getProcessEnv(), 262 | serverPath: sdk.lspPath, 263 | }; 264 | 265 | const module = this.extensionContext.asAbsolutePath( 266 | this.extensionContext.extensionMode == vscode.ExtensionMode.Development 267 | ? path.join('lsp-proxy', 'out', 'proxy.js') 268 | : path.join('out', 'proxy.js'), 269 | ); 270 | 271 | const serverOptions: vscodelc.ServerOptions = { 272 | run: { module, transport: TransportKind.ipc }, 273 | debug: { module, transport: TransportKind.ipc }, 274 | }; 275 | 276 | // Configure the client options. 277 | const clientOptions: vscodelc.LanguageClientOptions = { 278 | // The current selection mechanism indicates all documents to be served 279 | // by the same single LSP Server. This wouldn't work if at some point 280 | // we support multiple SDKs running at once, for which we'd need a more 281 | // flexible way to manage LSP Servers than `vscodelc`. Two options might 282 | // be feasible: 283 | // - Fork/contribute `vscodelc` and allow for a more customizable selection logic. 284 | // - Do the selection within the proxy, which would be "easy" to implement 285 | // if the proxy is restarted with the new correct info whenever a new SDK is 286 | // identified. 287 | documentSelector: [ 288 | { 289 | language: 'mojo', 290 | }, 291 | { 292 | scheme: 'vscode-notebook-cell', 293 | language: 'mojo', 294 | }, 295 | ], 296 | synchronize: { 297 | // Notify the server about file changes following the given file 298 | // pattern. 299 | fileEvents: vscode.workspace.createFileSystemWatcher( 300 | '**/*.{mojo,🔥,ipynb}', 301 | ), 302 | }, 303 | outputChannel: this.logger.lsp.outputChannel, 304 | 305 | // Don't switch to output window when the server returns output. 306 | revealOutputChannelOn: vscodelc.RevealOutputChannelOn.Never, 307 | initializationOptions: initializationOptions, 308 | }; 309 | 310 | clientOptions.middleware = { 311 | sendRequest: (method, param, token, next) => { 312 | if (this.recorder) { 313 | return this.recorder.sendRequest(method, param, token, next); 314 | } else { 315 | return next(method, param, token); 316 | } 317 | }, 318 | sendNotification: (method, next, param) => { 319 | if (this.recorder) { 320 | return this.recorder.sendNotification(method, next, param); 321 | } else { 322 | return next(method, param); 323 | } 324 | }, 325 | }; 326 | 327 | // Create the language client and start the client. 328 | const languageClient = new vscodelc.LanguageClient( 329 | 'mojo-lsp', 330 | 'Mojo Language Client', 331 | serverOptions, 332 | clientOptions, 333 | ); 334 | 335 | // The proxy sends us a mojo/lspRestart notification when it restarts the 336 | // underlying language server. It's our job to pass that to the telemetry 337 | // backend. 338 | this.pushSubscription( 339 | languageClient.onNotification('mojo/lspRestart', () => { 340 | this.reporter.sendTelemetryEvent('lspRestart', { 341 | mojoSDKVersion: sdk.version, 342 | mojoSDKKind: sdk.kind, 343 | }); 344 | }), 345 | ); 346 | 347 | this.logger.lsp.info( 348 | `Launching Language Server '${ 349 | initializationOptions.serverPath 350 | }' with options:`, 351 | initializationOptions.serverArgs, 352 | ); 353 | this.logger.lsp.info('Launching Language Server'); 354 | // We intentionally don't await the `start` so that we can cancelling it 355 | // during a long initialization, which can happen when in debug mode. 356 | languageClient.start(); 357 | return languageClient; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | ---- LLVM Exceptions to the Apache 2.0 License ---- 204 | 205 | As an exception, if, as a result of your compiling your source code, portions 206 | of this Software are embedded into an Object form of such source code, you 207 | may redistribute such embedded portions in such Object form without complying 208 | with the conditions of Sections 4(a), 4(b) and 4(d) of the License. 209 | 210 | In addition, if you combine or link compiled forms of this Software with 211 | software that is licensed under the GPLv2 ("Combined Software") and if a 212 | court of competent jurisdiction determines that the patent provision (Section 213 | 3), the indemnity provision (Section 9) or other Section of the License 214 | conflicts with the conditions of the GPLv2, you may retroactively and 215 | prospectively choose to deem waived or otherwise exclude such Section(s) of 216 | the License, but only in their entirety and only with respect to the Combined 217 | Software. 218 | 219 | ============================================================================== 220 | Software from third parties included in the LLVM Project: 221 | ============================================================================== 222 | 223 | The LLVM Project contains third party software which is under different license 224 | terms. All such code will be identified clearly using at least one of two 225 | mechanisms: 226 | 227 | 1) It will be in a separate directory tree with its own `LICENSE.txt` or 228 | `LICENSE` file at the top containing the specific license and restrictions 229 | which apply to that software, or 230 | 2) It will contain specific license and restriction terms at the top of every 231 | file. 232 | -------------------------------------------------------------------------------- /lsp-proxy/src/MojoLSPProxy.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import { 15 | DiagnosticSeverity, 16 | DidChangeNotebookDocumentParams, 17 | DidChangeTextDocumentParams, 18 | DidCloseNotebookDocumentParams, 19 | DidCloseTextDocumentParams, 20 | DidOpenNotebookDocumentParams, 21 | DidOpenTextDocumentParams, 22 | InitializeParams, 23 | InitializeResult, 24 | PublishDiagnosticsNotification, 25 | PublishDiagnosticsParams, 26 | } from 'vscode-languageserver-protocol'; 27 | import { 28 | createConnection as createClientConnection, 29 | ProposedFeatures, 30 | } from 'vscode-languageserver/node'; 31 | 32 | import { MojoDocument, MojoDocumentsStateHandler } from './MojoDocument'; 33 | import { MojoLSPServer } from './MojoLSPServer'; 34 | import { 35 | Client, 36 | ExitStatus, 37 | JSONObject, 38 | Optional, 39 | RequestParamsWithDocument, 40 | URI, 41 | } from './types'; 42 | 43 | /** 44 | * Class in charge of of managing the communication between the VSCode client 45 | * and the actual mojo-lsp-server. 46 | */ 47 | export class MojoLSPProxy { 48 | /** 49 | * The connection with the VSCode client. 50 | */ 51 | private client: Client; 52 | /** 53 | * The actual Mojo LSP Server. It'll be created as part of the `onInitialize` 54 | * method of the proxy. 55 | */ 56 | private server: Optional; 57 | /** 58 | * The state handler for all the documents notified by the client. 59 | */ 60 | private docsStateHandler: MojoDocumentsStateHandler; 61 | /** 62 | * The time when the proxy was initialized. 63 | */ 64 | private initTime = Date.now(); 65 | /** 66 | * The initialization params used to launch the server. They are gotten from 67 | * the client as part of the `initialize` request and have to be reused 68 | * whenever the server is restarted. 69 | */ 70 | private initializeParams: Optional; 71 | 72 | constructor() { 73 | this.client = createClientConnection(ProposedFeatures.all); 74 | this.docsStateHandler = new MojoDocumentsStateHandler(this.client); 75 | this.registerProxies(); 76 | } 77 | 78 | /** 79 | * Start the actual communication with the client. 80 | */ 81 | public start() { 82 | this.client.listen(); 83 | } 84 | 85 | /** 86 | * Create a the error message that will be display on the given document upon 87 | * a crash. 88 | */ 89 | private createDiagnosticErrorMessageUponCrash( 90 | doc: MojoDocument, 91 | crashTrigger: Optional, 92 | ): string { 93 | let errorMessage = 'A crash happened in the Mojo Language Server'; 94 | if (this.docsStateHandler.isCrashTrigger(doc)) { 95 | errorMessage += 96 | ' when processing this document. The Language Server will try to ' + 97 | 'reprocess this document once it is edited again.'; 98 | } else { 99 | if (crashTrigger !== undefined) { 100 | errorMessage += ' when processing ' + crashTrigger; 101 | } 102 | errorMessage += 103 | '. The Language Server will try to reprocess this ' + 104 | 'document automatically.'; 105 | } 106 | errorMessage += 107 | ' Please report this issue in ' + 108 | 'https://github.com/modular/modular/issues along with all the ' + 109 | 'relevant source codes with their current contents.'; 110 | return errorMessage; 111 | } 112 | 113 | /** 114 | * Whenever there's a restart, this clears the diagnostics for each tracked 115 | * file and adds one new diagnostic mentioning the crash. 116 | * We also mark the possible culprit doc appropriately. 117 | */ 118 | private prepareTrackedDocsForRestart() { 119 | this.docsStateHandler.urisTrackedByServer.clear(); 120 | // In order to identify the crash trigger, we use the simple heuristic of 121 | // assuming that the oldest pending request is the one that caused the 122 | // crash. This should work most the times, as most crashes should originate 123 | // when the server is processing a request. However, if the crash happens at 124 | // any other moment, e.g., when reading its stdin, we would need a more 125 | // complex mechanism to identify the actual issue. 126 | const crashTriggerURI = ( 127 | this.server?.getOldestPendingRequest() as Optional 128 | )?.textDocument?.uri; 129 | for (const doc of this.docsStateHandler.getAllDocs()) { 130 | if (doc.uri === crashTriggerURI) { 131 | this.docsStateHandler.markAsCrashTrigger(doc); 132 | } 133 | const errorMessage = this.createDiagnosticErrorMessageUponCrash( 134 | doc, 135 | crashTriggerURI, 136 | ); 137 | 138 | const diagnostic: PublishDiagnosticsParams = { 139 | diagnostics: [ 140 | { 141 | message: errorMessage, 142 | range: { 143 | start: { line: 0, character: 0 }, 144 | end: { line: 0, character: 0 }, 145 | }, 146 | severity: DiagnosticSeverity.Error, 147 | source: 'mojo', 148 | }, 149 | ], 150 | uri: doc.uri, 151 | version: doc.version, 152 | }; 153 | this.client.sendNotification( 154 | PublishDiagnosticsNotification.method, 155 | diagnostic, 156 | ); 157 | } 158 | } 159 | 160 | /** 161 | * Restart the server upon an unsuccessful termination of the server. This 162 | * will also issue an initialization request to the new server. 163 | */ 164 | private restartServer(status: ExitStatus) { 165 | this.client.console.log( 166 | `The mojo-lsp-server binary exited with signal '${ 167 | status.signal 168 | }' and exit code '${status.code}'.`, 169 | ); 170 | 171 | this.client.sendNotification('mojo/lspRestart'); 172 | 173 | const timeSinceInitInMillis = Date.now() - this.initTime; 174 | const timeSinceInitInMins = Math.floor(timeSinceInitInMillis / 60000); 175 | // We only allow one restart per minute to prevent VSCode from disabling the 176 | // LSP. VSCode allows 4 crashes every 3 minutes. 177 | if (timeSinceInitInMins >= 1) { 178 | this.client.console.log( 179 | `The mojo-lsp-server binary has exited unsuccessfully. The proxy will terminate. It ran ${ 180 | timeSinceInitInMins 181 | } ms.`, 182 | ); 183 | 184 | if (status.signal !== null) { 185 | process.kill(process.pid, status.signal); 186 | } 187 | process.exit(status.code!); 188 | } 189 | this.client.console.log(`The mojo-lsp-server will restart.`); 190 | this.prepareTrackedDocsForRestart(); 191 | this.server!.dispose(); 192 | this.initializeServer(); 193 | } 194 | 195 | /** 196 | * Spawn a new server and send the initialization request to it. 197 | * 198 | * @returns the response to the initialization request. 199 | */ 200 | private initializeServer(): Promise { 201 | const params = this.initializeParams!; 202 | const workspaceFolder = params.rootUri; 203 | this.client.console.log( 204 | `Server(${process.pid}) ${workspaceFolder} started`, 205 | ); 206 | 207 | this.server = new MojoLSPServer({ 208 | initializationOptions: params.initializationOptions, 209 | logger: (message: string) => this.client.console.log(message), 210 | onExit: (status: ExitStatus) => { 211 | // If the server exited successfully, then that's because a terminate 212 | // request was sent, so we just terminate the proxy as well. 213 | 214 | // If the server exited successfully, then that's because a terminate 215 | // request was sent, so we just terminate the proxy as well. 216 | if (status.code === 0) { 217 | process.exit(0); 218 | } 219 | // There's been an error, we'll try restart the server. 220 | // There's been an error, we'll try restart the server. 221 | this.restartServer(status); 222 | }, 223 | onNotification: (method: string, params: any) => 224 | this.client.sendNotification(method, params), 225 | onOutgoingRequest: async ( 226 | id: any, 227 | method: string, 228 | params: JSONObject, 229 | ) => { 230 | const result = await this.client.sendRequest(method, params); 231 | this.server!.sendResponse(id, result); 232 | }, 233 | }); 234 | return this.server!.sendRequest( 235 | params, 236 | 'initialize', 237 | ) as Promise; 238 | } 239 | 240 | /** 241 | * Register the individual proxies for all requests and client-sided 242 | * notifications supports by the mojo-lsp-server. 243 | */ 244 | private registerProxies() { 245 | // Initialize request is special because it contains the information we need 246 | // to launch the actual mojo-lsp-server. 247 | this.client.onInitialize(async (params) => { 248 | this.initializeParams = params; 249 | return this.initializeServer(); 250 | }); 251 | 252 | // Document-based requests 253 | // Note: all of these requests must go through `relayRequestWithDocument` to 254 | // ensure crash handling is applied correctly. 255 | this.client.onCodeAction( 256 | this.relayRequestWithDocument('textDocument/codeAction'), 257 | ); 258 | this.client.onCompletion( 259 | this.relayRequestWithDocument('textDocument/completion'), 260 | ); 261 | this.client.onDefinition( 262 | this.relayRequestWithDocument('textDocument/definition'), 263 | ); 264 | this.client.onDocumentSymbol( 265 | this.relayRequestWithDocument('textDocument/documentSymbol'), 266 | ); 267 | this.client.onFoldingRanges( 268 | this.relayRequestWithDocument('textDocument/foldingRange'), 269 | ); 270 | this.client.onHover(this.relayRequestWithDocument('textDocument/hover')); 271 | this.client.onReferences( 272 | this.relayRequestWithDocument('textDocument/references'), 273 | ); 274 | this.client.onRenameRequest( 275 | this.relayRequestWithDocument('textDocument/rename'), 276 | ); 277 | this.client.onSignatureHelp( 278 | this.relayRequestWithDocument('textDocument/signatureHelp'), 279 | ); 280 | this.client.onShutdown((params) => { 281 | return this.server!.sendRequest(params, 'shutdown') as Promise; 282 | }); 283 | this.client.languages.inlayHint.on( 284 | this.relayRequestWithDocument('textDocument/inlayHint'), 285 | ); 286 | this.client.languages.semanticTokens.on( 287 | this.relayRequestWithDocument('textDocument/semanticTokens/full'), 288 | ); 289 | this.client.languages.semanticTokens.onDelta( 290 | this.relayRequestWithDocument('textDocument/semanticTokens/full/delta'), 291 | ); 292 | 293 | // Client notifications - normal documents 294 | this.client.onDidOpenTextDocument((params: DidOpenTextDocumentParams) => { 295 | this.docsStateHandler.onDidOpenTextDocument(params, this.server!); 296 | }); 297 | 298 | this.client.onDidCloseTextDocument((params: DidCloseTextDocumentParams) => { 299 | this.docsStateHandler.onDidCloseTextDocument(params, this.server!); 300 | }); 301 | 302 | this.client.onDidChangeTextDocument( 303 | (params: DidChangeTextDocumentParams) => { 304 | this.docsStateHandler.onDidChangeTextDocument(params, this.server!); 305 | }, 306 | ); 307 | 308 | // Client notifications - notebooks 309 | const notebooks = this.client.notebooks.synchronization; 310 | notebooks.onDidOpenNotebookDocument( 311 | (params: DidOpenNotebookDocumentParams) => { 312 | this.docsStateHandler.onDidOpenNotebookDocument(params, this.server!); 313 | }, 314 | ); 315 | 316 | notebooks.onDidCloseNotebookDocument( 317 | (params: DidCloseNotebookDocumentParams) => { 318 | this.docsStateHandler.onDidCloseNotebookDocument(params, this.server!); 319 | }, 320 | ); 321 | 322 | notebooks.onDidChangeNotebookDocument( 323 | (params: DidChangeNotebookDocumentParams) => { 324 | this.docsStateHandler.onDidChangeNotebookDocument(params, this.server!); 325 | }, 326 | ); 327 | 328 | this.client.onNotification('mojo/emitParsedIR', (params) => { 329 | this.client.console.log(JSON.stringify(params)); 330 | this.server!.sendNotification(params, 'mojo/emitParsedIR'); 331 | }); 332 | } 333 | 334 | /** 335 | * This method should be used to relay requests that have a `textDocument.uri` 336 | * param. 337 | */ 338 | private relayRequestWithDocument(method: string) { 339 | return (params: RequestParamsWithDocument) => { 340 | const uri: URI = params.textDocument.uri; 341 | // If try to run a request on a document that is not tracked by the 342 | // server, then we need to reopen it because we just had a crash recently. 343 | // However, if it's a crash trigger, we don't reopen it and wait for edits 344 | // to happen first. 345 | const owningDoc = 346 | this.docsStateHandler.getOwningTextOrNotebookDocument(uri); 347 | 348 | if ( 349 | owningDoc !== undefined && 350 | !this.docsStateHandler.isCrashTrigger(owningDoc) && 351 | !this.docsStateHandler.isTrackedByServer(owningDoc) 352 | ) { 353 | owningDoc.openDocumentOnServer(this.server!, this.docsStateHandler); 354 | } 355 | return this.server!.sendRequest(params, method) as any; 356 | }; 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /extension/pyenv.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // Copyright (c) 2025, Modular Inc. All rights reserved. 3 | // 4 | // Licensed under the Apache License v2.0 with LLVM Exceptions: 5 | // https://llvm.org/LICENSE.txt 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | //===----------------------------------------------------------------------===// 13 | 14 | import * as vscode from 'vscode'; 15 | import * as ini from 'ini'; 16 | import { DisposableContext } from './utils/disposableContext'; 17 | import { PythonExtension, ResolvedEnvironment } from '@vscode/python-extension'; 18 | import assert from 'assert'; 19 | import { Logger } from './logging'; 20 | import path from 'path'; 21 | import * as util from 'util'; 22 | import { 23 | execFile as callbackExecFile, 24 | exec as callbackExec, 25 | } from 'child_process'; 26 | import { Memoize } from 'typescript-memoize'; 27 | import { TelemetryReporter } from './telemetry'; 28 | import { fileExists } from './utils/files'; 29 | const execFile = util.promisify(callbackExecFile); 30 | const exec = util.promisify(callbackExec); 31 | 32 | export enum SDKKind { 33 | Environment = 'environment', 34 | Custom = 'custom', 35 | Internal = 'internal', 36 | } 37 | 38 | /// Represents a usable instance of the MAX SDK. 39 | export class SDK { 40 | public readonly supportsFileDebug: boolean = false; 41 | 42 | constructor( 43 | private logger: Logger, 44 | /// What kind of SDK this is. Primarily used for logging and context hinting. 45 | readonly kind: SDKKind, 46 | /// The unparsed version string of the SDK. 47 | readonly version: string, 48 | /// The path to the language server executable. 49 | readonly lspPath: string, 50 | /// The path to the mblack executable. 51 | readonly mblackPath: string, 52 | /// The path to the Mojo LLDB plugin. 53 | readonly lldbPluginPath: string, 54 | /// The path to the DAP server executable. 55 | readonly dapPath: string, 56 | /// The path to the Mojo executable. 57 | readonly mojoPath: string, 58 | /// The path to the directory containing LLDB debug visualizers. 59 | readonly visualizersPath: string, 60 | /// The path to the LLDB executor. 61 | readonly lldbPath: string, 62 | ) {} 63 | 64 | @Memoize() 65 | /// Checks if the version of LLDB shipped with this SDK supports Python scripting. 66 | public async lldbHasPythonScriptingSupport(): Promise { 67 | try { 68 | let { stdout, stderr } = await execFile(this.lldbPath, [ 69 | '-b', 70 | '-o', 71 | 'script print(100+1)', 72 | ]); 73 | stdout = (stdout || '') as string; 74 | stderr = (stderr || '') as string; 75 | 76 | if (stdout.indexOf('101') != -1) { 77 | this.logger.info('Python scripting support in LLDB found.'); 78 | return true; 79 | } else { 80 | this.logger.info( 81 | `Python scripting support in LLDB not found. The test script returned:\n${ 82 | stdout 83 | }\n${stderr}`, 84 | ); 85 | } 86 | } catch (e) { 87 | this.logger.error( 88 | 'Python scripting support in LLDB not found. The test script failed with', 89 | e, 90 | ); 91 | } 92 | return false; 93 | } 94 | 95 | /// Gets an appropriate environment to spawn subprocesses from this SDK. 96 | public getProcessEnv(withTelemetry: boolean = true) { 97 | return { 98 | MODULAR_TELEMETRY_ENABLED: withTelemetry ? 'true' : 'false', 99 | }; 100 | } 101 | } 102 | 103 | class HomeSDK extends SDK { 104 | public override readonly supportsFileDebug: boolean = true; 105 | 106 | constructor( 107 | logger: Logger, 108 | kind: SDKKind, 109 | version: string, 110 | private homePath: string, 111 | lspPath: string, 112 | mblackPath: string, 113 | lldbPluginPath: string, 114 | dapPath: string, 115 | mojoPath: string, 116 | visualizersPath: string, 117 | lldbPath: string, 118 | private prefixPath?: string, 119 | ) { 120 | super( 121 | logger, 122 | kind, 123 | version, 124 | lspPath, 125 | mblackPath, 126 | lldbPluginPath, 127 | dapPath, 128 | mojoPath, 129 | visualizersPath, 130 | lldbPath, 131 | ); 132 | } 133 | 134 | public override getProcessEnv(withTelemetry: boolean = true) { 135 | return { 136 | ...super.getProcessEnv(withTelemetry), 137 | MODULAR_HOME: this.homePath, 138 | // HACK: Set CONDA_PREFIX to allow debugger wrappers to work 139 | CONDA_PREFIX: this.prefixPath, 140 | }; 141 | } 142 | } 143 | 144 | export class PythonEnvironmentManager extends DisposableContext { 145 | private api: PythonExtension | undefined = undefined; 146 | private logger: Logger; 147 | private reporter: TelemetryReporter; 148 | public onEnvironmentChange: vscode.Event; 149 | private envChangeEmitter: vscode.EventEmitter; 150 | private displayedSDKError: boolean = false; 151 | private lastLoadedEnv: string | undefined = undefined; 152 | 153 | constructor(logger: Logger, reporter: TelemetryReporter) { 154 | super(); 155 | this.logger = logger; 156 | this.reporter = reporter; 157 | this.envChangeEmitter = new vscode.EventEmitter(); 158 | this.onEnvironmentChange = this.envChangeEmitter.event; 159 | } 160 | 161 | public async init() { 162 | this.api = await PythonExtension.api(); 163 | this.pushSubscription( 164 | this.api.environments.onDidChangeActiveEnvironmentPath((p) => 165 | this.handleEnvironmentChange(p.path), 166 | ), 167 | ); 168 | } 169 | 170 | private async handleEnvironmentChange(newEnv: string) { 171 | this.logger.debug( 172 | `Active environment path change: ${newEnv} (current: ${this.lastLoadedEnv})`, 173 | ); 174 | if (newEnv != this.lastLoadedEnv) { 175 | this.logger.info('Python environment has changed, reloading SDK'); 176 | this.envChangeEmitter.fire(); 177 | this.displayedSDKError = false; 178 | } 179 | } 180 | 181 | /// Load the active SDK from the currently active Python environment, or undefined if one is not present. 182 | public async getActiveSDK(): Promise { 183 | assert(this.api !== undefined); 184 | // Prioritize retrieving a monorepo SDK over querying the environment. 185 | const monorepoSDK = await this.tryGetMonorepoSDK(); 186 | 187 | if (monorepoSDK) { 188 | this.logger.info( 189 | 'Monorepo SDK found, prioritizing that over Python environment.', 190 | ); 191 | return monorepoSDK; 192 | } 193 | 194 | const envPath = this.api.environments.getActiveEnvironmentPath(); 195 | const env = await this.api.environments.resolveEnvironment(envPath); 196 | this.logger.info('Loading MAX SDK information from Python environment'); 197 | this.lastLoadedEnv = envPath.path; 198 | 199 | if (!env) { 200 | this.logger.error( 201 | 'No Python enviroment could be retrieved from the Python extension.', 202 | ); 203 | await this.displaySDKError( 204 | 'Unable to load a Python enviroment from the VS Code Python extension.', 205 | ); 206 | return undefined; 207 | } 208 | 209 | // We cannot use the environment type information reported by the Python 210 | // extension because it considers Conda and wheel-based installs to be the 211 | // same, when we need to differentiate them. 212 | this.logger.info(`Found Python environment at ${envPath.path}`, env); 213 | if (await this.envHasModularCfg(env)) { 214 | this.logger.info( 215 | `Python environment '${envPath.path}' appears to be Conda-like; using modular.cfg method.`, 216 | ); 217 | return this.createSDKFromHomePath( 218 | SDKKind.Environment, 219 | path.join(env.executable.sysPrefix, 'share', 'max'), 220 | env.executable.sysPrefix, 221 | ); 222 | } else { 223 | this.logger.info( 224 | `Python environment '${envPath.path}' does not have a modular.cfg file; assuming wheel installation.`, 225 | ); 226 | return this.createSDKFromWheelEnv(env); 227 | } 228 | } 229 | 230 | private async displaySDKError(message: string) { 231 | if (this.displayedSDKError) { 232 | return; 233 | } 234 | 235 | this.displayedSDKError = true; 236 | await vscode.window.showErrorMessage(message); 237 | } 238 | 239 | private async envHasModularCfg(env: ResolvedEnvironment): Promise { 240 | return fileExists( 241 | path.join(env.executable.sysPrefix, 'share', 'max', 'modular.cfg'), 242 | ); 243 | } 244 | 245 | private async createSDKFromWheelEnv( 246 | env: ResolvedEnvironment, 247 | ): Promise { 248 | const binPath = path.join(env.executable.sysPrefix, 'bin'); 249 | const libPath = path.join( 250 | env.executable.sysPrefix, 251 | 'lib', 252 | `python${env.version!.major}.${env.version!.minor}`, 253 | 'site-packages', 254 | 'modular', 255 | 'lib', 256 | ); 257 | // helper to pull required files/folders out of the environment 258 | const retrievePath = async (target: string) => { 259 | this.logger.debug(`Retrieving tool path '${target}'.`); 260 | try { 261 | // stat-ing the path confirms it exists in some form; if an exception is thrown then it doesn't exist. 262 | await vscode.workspace.fs.stat(vscode.Uri.file(target)); 263 | return target; 264 | } catch { 265 | this.logger.error(`Missing path ${target} in venv.`); 266 | return undefined; 267 | } 268 | }; 269 | 270 | const libExt = process.platform == 'darwin' ? 'dylib' : 'so'; 271 | 272 | const mojoPath = await retrievePath(path.join(binPath, 'mojo')); 273 | const lspPath = await retrievePath(path.join(binPath, 'mojo-lsp-server')); 274 | const lldbPluginPath = await retrievePath( 275 | path.join(libPath, `libMojoLLDB.${libExt}`), 276 | ); 277 | const mblackPath = await retrievePath(path.join(binPath, 'mblack')); 278 | const dapPath = await retrievePath(path.join(binPath, 'lldb-dap')); 279 | const visualizerPath = await retrievePath( 280 | path.join(libPath, 'lldb-visualizers'), 281 | ); 282 | const lldbPath = await retrievePath(path.join(binPath, 'mojo-lldb')); 283 | // The debugger requires that we avoid using the wrapped `mojo` entrypoint for specific scenarios. 284 | const rawMojoPath = await retrievePath( 285 | path.join(libPath, '..', 'bin', 'mojo'), 286 | ); 287 | 288 | if ( 289 | !mojoPath || 290 | !lspPath || 291 | !lldbPluginPath || 292 | !rawMojoPath || 293 | !mblackPath || 294 | !lldbPluginPath || 295 | !dapPath || 296 | !visualizerPath || 297 | !lldbPath 298 | ) { 299 | return undefined; 300 | } 301 | 302 | // We don't know the version intrinsically so we need to invoke it ourselves. 303 | const versionResult = await exec(`"${mojoPath}" --version`); 304 | return new SDK( 305 | this.logger, 306 | SDKKind.Environment, 307 | versionResult.stdout, 308 | lspPath, 309 | mblackPath, 310 | lldbPluginPath, 311 | dapPath, 312 | mojoPath, 313 | visualizerPath, 314 | lldbPath, 315 | ); 316 | } 317 | 318 | /// Attempts to create a SDK from a home path. Returns undefined if creation failed. 319 | public async createSDKFromHomePath( 320 | kind: SDKKind, 321 | homePath: string, 322 | prefixPath?: string, 323 | ): Promise { 324 | const modularCfgPath = path.join(homePath, 'modular.cfg'); 325 | const decoder = new TextDecoder(); 326 | let bytes; 327 | try { 328 | bytes = await vscode.workspace.fs.readFile( 329 | vscode.Uri.file(modularCfgPath), 330 | ); 331 | } catch (e) { 332 | await this.displaySDKError(`Unable to read modular.cfg: ${e}`); 333 | this.logger.error('Error reading modular.cfg', e); 334 | return undefined; 335 | } 336 | 337 | let contents; 338 | try { 339 | contents = decoder.decode(bytes); 340 | } catch (e) { 341 | await this.displaySDKError( 342 | 'Unable to decode modular.cfg; your MAX installation may be corrupted.', 343 | ); 344 | this.logger.error('Error decoding modular.cfg bytes to string', e); 345 | return undefined; 346 | } 347 | 348 | let config; 349 | try { 350 | config = ini.parse(contents); 351 | } catch (e) { 352 | await this.displaySDKError( 353 | 'Unable to parse modular.cfg; your MAX installation may be corrupted.', 354 | ); 355 | this.logger.error('Error parsing modular.cfg contents as INI', e); 356 | return undefined; 357 | } 358 | 359 | try { 360 | const version = 'version' in config.max ? config.max.version : '0.0.0'; 361 | this.logger.info(`Found SDK with version ${version}`); 362 | 363 | this.reporter.sendTelemetryEvent('sdkLoaded', { 364 | version, 365 | kind, 366 | }); 367 | 368 | return new HomeSDK( 369 | this.logger, 370 | kind, 371 | version, 372 | homePath, 373 | config['mojo-max']['lsp_server_path'], 374 | config['mojo-max']['mblack_path'], 375 | config['mojo-max']['lldb_plugin_path'], 376 | config['mojo-max']['lldb_vscode_path'], 377 | config['mojo-max']['driver_path'], 378 | config['mojo-max']['lldb_visualizers_path'], 379 | config['mojo-max']['lldb_path'], 380 | prefixPath, 381 | ); 382 | } catch (e) { 383 | await this.displaySDKError( 384 | 'Unable to read a configuration key from modular.cfg; your MAX installation may be corrupted.', 385 | ); 386 | this.logger.error('Error creating SDK from modular.cfg', e); 387 | return undefined; 388 | } 389 | } 390 | 391 | /// Attempt to load a monorepo SDK from the currently open workspace folder. 392 | /// Resolves with the loaded SDK, or undefined if one doesn't exist. 393 | private async tryGetMonorepoSDK(): Promise { 394 | if (!vscode.workspace.workspaceFolders) { 395 | return; 396 | } 397 | 398 | if (vscode.workspace.workspaceFolders.length !== 1) { 399 | return; 400 | } 401 | 402 | const folder = vscode.Uri.joinPath( 403 | vscode.workspace.workspaceFolders[0].uri, 404 | '.derived', 405 | ); 406 | try { 407 | const info = await vscode.workspace.fs.stat(folder); 408 | if (info.type & vscode.FileType.Directory) { 409 | return this.createSDKFromHomePath(SDKKind.Internal, folder.fsPath); 410 | } 411 | } catch { 412 | return undefined; 413 | } 414 | } 415 | } 416 | --------------------------------------------------------------------------------