├── fixtures ├── .gitignore ├── another-lib-project │ ├── src │ │ └── lib.rs │ └── Cargo.toml └── bare-lib-project │ ├── src │ └── lib.rs │ └── Cargo.toml ├── icon.png ├── .gitignore ├── prettier.config.js ├── rust-analyzer └── editors │ └── code │ ├── .gitignore │ ├── icon.png │ ├── .vscodeignore │ ├── README.md │ ├── rollup.config.js │ ├── tsconfig.json │ ├── ra_syntax_tree.tmGrammar.json │ ├── .eslintrc.js │ ├── tests │ ├── unit │ │ ├── index.ts │ │ ├── launch_config.test.ts │ │ └── runnable_env.test.ts │ └── runTests.ts │ └── src │ ├── persistent_state.ts │ ├── snippets.ts │ ├── ctx.ts │ ├── lsp_ext.ts │ ├── tasks.ts │ ├── util.ts │ ├── net.ts │ ├── run.ts │ ├── config.ts │ ├── debug.ts │ ├── toolchain.ts │ ├── ast_inspector.ts │ ├── inlay_hints.ts │ └── client.ts ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── .prettierignore ├── src ├── spinner.ts ├── utils │ ├── observable.ts │ └── workspace.ts ├── configuration.ts ├── net.ts ├── tasks.ts ├── providers │ └── signatureHelpProvider.ts ├── rls.ts ├── rustup.ts ├── rustAnalyzer.ts └── extension.ts ├── cmd └── check-version.js ├── .vscodeignore ├── tsconfig.json ├── test ├── runTest.ts └── suite │ ├── rustup.test.ts │ ├── index.ts │ └── extension.test.ts ├── language-configuration.json ├── LICENSE-MIT ├── COPYRIGHT ├── .github └── workflows │ ├── release.yml │ └── nodejs.yml ├── .eslintrc.json ├── snippets └── rust.json ├── contributing.md ├── README.md ├── CHANGELOG.md └── LICENSE-APACHE /fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/vscode-rust/HEAD/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test 4 | *.vsix 5 | rls*.log 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | }; 5 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | bundle 6 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/vscode-rust/HEAD/rust-analyzer/editors/code/icon.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-vscode.vscode-typescript-tslint-plugin", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/another-lib-project/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn it_works() { 5 | assert_eq!(2 + 2, 4); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/bare-lib-project/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn it_works() { 5 | assert_eq!(2 + 2, 4); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # FIXME: Only ignore the pending changes to be merged into this extension 2 | /rust-analyzer 3 | 4 | # From .gitignore 5 | out 6 | node_modules 7 | .vscode-test 8 | -------------------------------------------------------------------------------- /fixtures/another-lib-project/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bare-lib-project" 3 | version = "0.1.0" 4 | authors = ["Example "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /fixtures/bare-lib-project/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bare-lib-project" 3 | version = "0.1.0" 4 | authors = ["Example "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | !out/src/main.js 3 | !package.json 4 | !package-lock.json 5 | !ra_syntax_tree.tmGrammar.json 6 | !rust.tmGrammar.json 7 | !icon.png 8 | !README.md 9 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/README.md: -------------------------------------------------------------------------------- 1 | # rust-analyzer 2 | 3 | Provides support for rust-analyzer: novel LSP server for the Rust programming language. 4 | 5 | See https://rust-analyzer.github.io/ for more information. 6 | -------------------------------------------------------------------------------- /src/spinner.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | 3 | export function startSpinner(message: string) { 4 | window.setStatusBarMessage(`Rust: $(settings-gear~spin) ${message}`); 5 | } 6 | 7 | export function stopSpinner(message?: string) { 8 | window.setStatusBarMessage(message ? `Rust: ${message}` : 'Rust'); 9 | } 10 | -------------------------------------------------------------------------------- /cmd/check-version.js: -------------------------------------------------------------------------------- 1 | const { version } = require('../package'); 2 | const changelog = String(require('fs').readFileSync('CHANGELOG.md')).split('\n'); 3 | if (!changelog.find(line => line.startsWith('### ' + version))) { 4 | throw new Error(`The package.json version ${version} does not seem to have a matching heading in CHANGELOG.md`); 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.formatOnSave": true 4 | }, 5 | "files.exclude": { 6 | "out": false 7 | }, 8 | "search.exclude": { 9 | "out": true 10 | }, 11 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 12 | } 13 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .vscode/** 3 | .vscode-test/** 4 | fixtures/** 5 | test/** 6 | out/test/** 7 | src/** 8 | .gitignore 9 | **/.prettierignore 10 | **/.eslintignore 11 | prettier.config.js 12 | .eslintrc.json 13 | vsc-extension-quickstart.md 14 | **/tsconfig.json 15 | **/tslint.json 16 | **/*.map 17 | **/*.ts 18 | **/tsconfig.esm* 19 | node_modules/**/README.md 20 | node_modules/**/CHANGELOG.md 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["es6"], 7 | "sourceMap": true, 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true 13 | }, 14 | "include": ["src", "test"], 15 | "exclude": [ 16 | "node_modules", 17 | ".vscode-test" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/rollup.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import commonjs from '@rollup/plugin-commonjs'; 5 | import nodeBuiltins from 'builtin-modules'; 6 | 7 | /** @type { import('rollup').RollupOptions } */ 8 | export default { 9 | input: 'out/src/main.js', 10 | plugins: [ 11 | resolve({ 12 | preferBuiltins: true 13 | }), 14 | commonjs() 15 | ], 16 | external: [...nodeBuiltins, 'vscode'], 17 | output: { 18 | file: './out/src/main.js', 19 | format: 'cjs' 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "outDir": "out", 6 | "lib": [ 7 | "es2019" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": ".", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "newLine": "LF" 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | ".vscode-test" 21 | ], 22 | "include": [ 23 | "src", 24 | "tests" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { runTests } from 'vscode-test'; 3 | 4 | (async () => { 5 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 6 | const extensionTestsPath = path.resolve(__dirname, './suite'); 7 | 8 | await runTests({ 9 | extensionDevelopmentPath, 10 | extensionTestsPath, 11 | launchArgs: [ 12 | '--disable-extensions', 13 | // Already start in the fixtures dir because we lose debugger connection 14 | // once we re-open a different folder due to window reloading 15 | path.join(extensionDevelopmentPath, 'fixtures'), 16 | ], 17 | }).catch(() => { 18 | console.error(`Test run failed`); 19 | process.exit(1); 20 | }); 21 | })(); 22 | -------------------------------------------------------------------------------- /test/suite/rustup.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as child_process from 'child_process'; 3 | 4 | import * as rustup from '../../src/rustup'; 5 | 6 | // We need to ensure that rustup works and is installed 7 | const rustupVersion = child_process.execSync('rustup --version').toString(); 8 | assert(rustupVersion); 9 | 10 | const config: rustup.RustupConfig = { 11 | path: 'rustup', 12 | channel: 'stable', 13 | }; 14 | 15 | suite('Rustup Tests', () => { 16 | test('getVersion', async () => { 17 | const version = await rustup.getVersion(config); 18 | assert(rustupVersion.includes(`rustup ${version}`)); 19 | }); 20 | test('getActiveChannel', async () => { 21 | rustup.getActiveChannel('.', config.path); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": [ "/*", "*/" ] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"], 10 | ["<", ">"] 11 | ], 12 | "autoClosingPairs": [ 13 | { "open": "{", "close": "}" }, 14 | { "open": "[", "close": "]" }, 15 | { "open": "(", "close": ")" }, 16 | { "open": "<", "close": ">" }, 17 | { "open": "\"", "close": "\"", "notIn": ["string"] }, 18 | { "open": "/**", "close": " */", "notIn": ["string"] }, 19 | { "open": "/*!", "close": " */", "notIn": ["string"] } 20 | ], 21 | "surroundingPairs": [ 22 | ["{", "}"], 23 | ["[", "]"], 24 | ["(", ")"], 25 | ["<", ">"], 26 | ["'", "'"], 27 | ["\"", "\""] 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as glob from 'glob'; 2 | import * as Mocha from 'mocha'; 3 | import * as path from 'path'; 4 | 5 | export function run( 6 | testsRoot: string, 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | cb: (error: any, failures?: number) => void, 9 | ): void { 10 | // Create the mocha test 11 | const mocha = new Mocha({ 12 | ui: 'tdd', 13 | }).useColors(true); 14 | 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return cb(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | cb(null, failures); 27 | }); 28 | } catch (err) { 29 | cb(err); 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/ra_syntax_tree.tmGrammar.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | 4 | "scopeName": "source.ra_syntax_tree", 5 | "patterns": [ 6 | { "include": "#node_type" }, 7 | { "include": "#node_range_index" }, 8 | { "include": "#token_text" } 9 | ], 10 | "repository": { 11 | "node_type": { 12 | "match": "^\\s*([A-Z_][A-Z_0-9]*?)@", 13 | "captures": { 14 | "1": { 15 | "name": "entity.name.class" 16 | } 17 | } 18 | }, 19 | "node_range_index": { 20 | "match": "\\d+", 21 | "name": "constant.numeric" 22 | }, 23 | "token_text": { 24 | "match": "\".+\"", 25 | "name": "string" 26 | } 27 | }, 28 | "fileTypes": [ 29 | "rast" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/observable.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'vscode'; 2 | 3 | /** 4 | * A wrapper around a value of type `T` that can be subscribed to whenever the 5 | * underlying value changes. 6 | */ 7 | export class Observable { 8 | private _listeners: Set<(arg: T) => void> = new Set(); 9 | private _value: T; 10 | /** Returns the current value. */ 11 | get value() { 12 | return this._value; 13 | } 14 | /** Every change to the value triggers all the registered callbacks. */ 15 | set value(value: T) { 16 | this._value = value; 17 | this._listeners.forEach(fn => fn(value)); 18 | } 19 | 20 | constructor(value: T) { 21 | this._value = value; 22 | } 23 | 24 | /** 25 | * Registers a listener function that's called whenever the underlying value 26 | * changes. 27 | * @returns a function that unregisters the listener when called. 28 | */ 29 | public observe(fn: (arg: T) => void): Disposable { 30 | this._listeners.add(fn); 31 | 32 | return { dispose: () => this._listeners.delete(fn) }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The Rust Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 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 | { 6 | "version": "0.2.0", 7 | "configurations": [{ 8 | "name": "Run Extension", 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "runtimeExecutable": "${execPath}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "npm: watch" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/out/test/suite", 28 | "${workspaceFolder}/fixtures" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: watch" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "parserOptions": { 8 | "project": "tsconfig.json", 9 | "sourceType": "module" 10 | }, 11 | "plugins": [ 12 | "@typescript-eslint" 13 | ], 14 | "rules": { 15 | "camelcase": ["error"], 16 | "eqeqeq": ["error", "always", { "null": "ignore" }], 17 | "no-console": ["error"], 18 | "prefer-const": "error", 19 | "@typescript-eslint/member-delimiter-style": [ 20 | "error", 21 | { 22 | "multiline": { 23 | "delimiter": "semi", 24 | "requireLast": true 25 | }, 26 | "singleline": { 27 | "delimiter": "semi", 28 | "requireLast": false 29 | } 30 | } 31 | ], 32 | "@typescript-eslint/semi": [ 33 | "error", 34 | "always" 35 | ], 36 | "@typescript-eslint/no-unnecessary-type-assertion": "error" 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/tests/unit/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = __dirname; 13 | 14 | return new Promise((resolve, reject) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return reject(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.timeout(100000); 26 | mocha.run(failures => { 27 | if (failures > 0) { 28 | reject(new Error(`${failures} tests failed.`)); 29 | } else { 30 | resolve(); 31 | } 32 | }); 33 | } catch (err) { 34 | reject(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/persistent_state.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { log } from './util'; 3 | 4 | export class PersistentState { 5 | constructor(private readonly globalState: vscode.Memento) { 6 | const { lastCheck, releaseId, serverVersion } = this; 7 | log.info("PersistentState:", { lastCheck, releaseId, serverVersion }); 8 | } 9 | 10 | /** 11 | * Used to check for *nightly* updates once an hour. 12 | */ 13 | get lastCheck(): number | undefined { 14 | return this.globalState.get("lastCheck"); 15 | } 16 | async updateLastCheck(value: number) { 17 | await this.globalState.update("lastCheck", value); 18 | } 19 | 20 | /** 21 | * Release id of the *nightly* extension. 22 | * Used to check if we should update. 23 | */ 24 | get releaseId(): number | undefined { 25 | return this.globalState.get("releaseId"); 26 | } 27 | async updateReleaseId(value: number) { 28 | await this.globalState.update("releaseId", value); 29 | } 30 | 31 | /** 32 | * Version of the extension that installed the server. 33 | * Used to check if we need to update the server. 34 | */ 35 | get serverVersion(): string | undefined { 36 | return this.globalState.get("serverVersion"); 37 | } 38 | async updateServerVersion(value: string | undefined) { 39 | await this.globalState.update("serverVersion", value); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/tests/runTests.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | 4 | import { runTests } from 'vscode-test'; 5 | 6 | async function main() { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // Minimum supported version. 12 | const jsonData = fs.readFileSync(path.join(extensionDevelopmentPath, 'package.json')); 13 | const json = JSON.parse(jsonData.toString()); 14 | let minimalVersion: string = json.engines.vscode; 15 | if (minimalVersion.startsWith('^')) minimalVersion = minimalVersion.slice(1); 16 | 17 | const launchArgs = ["--disable-extensions"]; 18 | 19 | // All test suites (either unit tests or integration tests) should be in subfolders. 20 | const extensionTestsPath = path.resolve(__dirname, './unit/index'); 21 | 22 | // Run tests using the minimal supported version. 23 | await runTests({ 24 | version: minimalVersion, 25 | launchArgs, 26 | extensionDevelopmentPath, 27 | extensionTestsPath 28 | }); 29 | 30 | // and the latest one 31 | await runTests({ 32 | version: 'stable', 33 | launchArgs, 34 | extensionDevelopmentPath, 35 | extensionTestsPath 36 | }); 37 | } 38 | 39 | main().catch(err => { 40 | // eslint-disable-next-line no-console 41 | console.error('Failed to run tests', err); 42 | process.exit(1); 43 | }); 44 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Short version for non-lawyers: 2 | 3 | The RLS Project is dual-licensed under Apache 2.0 and MIT 4 | terms. 5 | 6 | 7 | Longer version: 8 | 9 | The RLS Project is copyright 2010, The RLS Developers. 10 | 11 | Licensed under the Apache License, Version 2.0 12 | or the MIT 14 | license , 15 | at your option. All files in the project carrying such 16 | notice may not be copied, modified, or distributed except 17 | according to those terms. 18 | 19 | * Additional copyright may be retained by contributors other 20 | than Mozilla, the RLS Developers, or the parties 21 | enumerated in this file. Such copyright can be determined 22 | on a case-by-case basis by examining the author of each 23 | portion of a file in the revision-control commit records 24 | of the project, or by consulting representative comments 25 | claiming copyright ownership for a file. 26 | 27 | For example, the text: 28 | 29 | "Copyright (c) 2011 Google Inc." 30 | 31 | appears in some files, and these files thereby denote 32 | that their author and copyright-holder is Google Inc. 33 | 34 | In all such cases, the absence of explicit licensing text 35 | indicates that the contributor chose to license their work 36 | for distribution under identical terms to those Mozilla 37 | has chosen for the collective work, enumerated at the top 38 | of this file. The only difference is the retention of 39 | copyright itself, held by the contributor. 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | 7 | name: Upload Release Asset 8 | 9 | jobs: 10 | build: 11 | name: Upload Release Asset 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Use Node.js 12.8.1 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: '12.8.1' 20 | - name: Build extension package 21 | run: | 22 | npm ci 23 | npx vsce package -o rust.vsix 24 | - name: Create Release 25 | id: create_release 26 | uses: actions/create-release@v1 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | with: 30 | tag_name: ${{ github.ref }} 31 | release_name: Release ${{ github.ref }} 32 | draft: false 33 | prerelease: false 34 | - name: Upload Release Asset 35 | id: upload-release-asset 36 | uses: actions/upload-release-asset@v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 41 | asset_path: ./rust.vsix 42 | asset_name: rust.vsix 43 | asset_content_type: application/vsix 44 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: VSCode + Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | # Lock to the version shipped with VSCode 1.43+ 17 | node-version: ['12.8.1'] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install latest Rust stable toolchain 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: stable 29 | profile: minimal 30 | - run: npm ci 31 | - run: npm run prettier -- --list-different 32 | - run: npm run compile 33 | - run: npm run lint 34 | - run: npm audit 35 | # Ensure we run our tests with a display set for Linux (not provided by default) 36 | - run: xvfb-run -a npm test 37 | if: runner.os == 'Linux' 38 | - run: npm test 39 | if: runner.os != 'Linux' 40 | 41 | # https://forge.rust-lang.org/infra/docs/bors.html#adding-a-new-repository-to-bors 42 | build_result: 43 | name: bors build finished 44 | runs-on: ubuntu-latest 45 | needs: ["build"] 46 | steps: 47 | - name: Mark the job as successful 48 | run: exit 0 49 | if: success() 50 | - name: Mark the job as unsuccessful 51 | run: exit 1 52 | if: "!success()" 53 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "prettier", 11 | "prettier/@typescript-eslint" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "project": "tsconfig.json", 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["@typescript-eslint"], 19 | "rules": { 20 | "@typescript-eslint/no-unused-vars": [ 21 | "warn", 22 | { 23 | "argsIgnorePattern": "^_" 24 | } 25 | ], 26 | "eqeqeq": ["error", "always", { "null": "ignore" }], 27 | "prefer-const": "error", 28 | "@typescript-eslint/member-delimiter-style": [ 29 | "error", 30 | { 31 | "multiline": { 32 | "delimiter": "semi", 33 | "requireLast": true 34 | }, 35 | "singleline": { 36 | "delimiter": "semi", 37 | "requireLast": false 38 | } 39 | } 40 | ], 41 | "@typescript-eslint/semi": [ 42 | "error", 43 | "always" 44 | ], 45 | // TODO: Silenced during TSLint -> ESLint conversion; consider enabling them 46 | "no-useless-escape": "off", 47 | "@typescript-eslint/no-non-null-assertion": "off", 48 | "@typescript-eslint/explicit-module-boundary-types": "off", 49 | "@typescript-eslint/no-floating-promises": "off", 50 | "@typescript-eslint/restrict-template-expressions": "off", 51 | "@typescript-eslint/no-unsafe-member-access": "off", 52 | "@typescript-eslint/no-unsafe-assignment": "off", 53 | "@typescript-eslint/require-await": "off" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /snippets/rust.json: -------------------------------------------------------------------------------- 1 | { 2 | "extern crate": { 3 | "prefix": "extern crate", 4 | "body": [ 5 | "extern crate ${1:name};" 6 | ], 7 | "description": "Insert extern crate" 8 | }, 9 | "for": { 10 | "prefix": "for", 11 | "body": [ 12 | "for ${1:elem} in ${2:iter} {", 13 | "\t$0", 14 | "}" 15 | ], 16 | "description": "Insert for loop" 17 | }, 18 | "macro_rules": { 19 | "prefix": "macro_rules", 20 | "body": [ 21 | "macro_rules! $1 {", 22 | "\t($2) => {", 23 | "\t\t$0", 24 | "\t};", 25 | "}" 26 | ], 27 | "description": "Insert macro_rules!" 28 | }, 29 | "if let": { 30 | "prefix": "if let", 31 | "body": [ 32 | "if let ${1:pattern} = ${2:value} {", 33 | "\t$3", 34 | "}" 35 | ], 36 | "description": "Insert if to match a specific pattern, useful for enum variants e.g. `Some(inner)`" 37 | }, 38 | "spawn": { 39 | "prefix": [ 40 | "thread_spawn", 41 | "spawn" 42 | ], 43 | "body": [ 44 | "std::thread::spawn(move || {", 45 | "\t$1", 46 | "})" 47 | ], 48 | "description": "Wrap code in thread::spawn" 49 | }, 50 | "derive": { 51 | "prefix": "derive", 52 | "body": [ 53 | "#[derive(${1})]" 54 | ], 55 | "description": "#[derive(…)]" 56 | }, 57 | "cfg": { 58 | "prefix": "cfg", 59 | "body": [ 60 | "#[cfg(${1})]" 61 | ], 62 | "description": "#[cfg(…)]" 63 | }, 64 | "test": { 65 | "prefix": "test", 66 | "body": [ 67 | "#[test]", 68 | "fn ${1:name}() {", 69 | " ${2:unimplemented!();}", 70 | "}" 71 | ], 72 | "description": "#[test]" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/tests/unit/launch_config.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { Cargo } from '../../src/toolchain'; 3 | 4 | suite('Launch configuration', () => { 5 | 6 | suite('Lens', () => { 7 | test('A binary', async () => { 8 | const args = Cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "pkg_name"]); 9 | 10 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]); 11 | assert.deepEqual(args.filter, undefined); 12 | }); 13 | 14 | test('One of Multiple Binaries', async () => { 15 | const args = Cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "bin1"]); 16 | 17 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin1", "--message-format=json"]); 18 | assert.deepEqual(args.filter, undefined); 19 | }); 20 | 21 | test('A test', async () => { 22 | const args = Cargo.artifactSpec(["test", "--package", "pkg_name", "--lib", "--no-run"]); 23 | 24 | assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--no-run", "--message-format=json"]); 25 | assert.notDeepEqual(args.filter, undefined); 26 | }); 27 | }); 28 | 29 | suite('QuickPick', () => { 30 | test('A binary', async () => { 31 | const args = Cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "pkg_name"]); 32 | 33 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]); 34 | assert.deepEqual(args.filter, undefined); 35 | }); 36 | 37 | 38 | test('One of Multiple Binaries', async () => { 39 | const args = Cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "bin2"]); 40 | 41 | assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin2", "--message-format=json"]); 42 | assert.deepEqual(args.filter, undefined); 43 | }); 44 | 45 | test('A test', async () => { 46 | const args = Cargo.artifactSpec(["test", "--package", "pkg_name", "--lib"]); 47 | 48 | assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--message-format=json", "--no-run"]); 49 | assert.notDeepEqual(args.filter, undefined); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/utils/workspace.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { Uri, workspace, WorkspaceFolder } from 'vscode'; 4 | 5 | // searches up the folder structure until it finds a Cargo.toml 6 | export function nearestParentWorkspace( 7 | curWorkspace: WorkspaceFolder, 8 | filePath: string, 9 | ): WorkspaceFolder { 10 | // check that the workspace folder already contains the "Cargo.toml" 11 | const workspaceRoot = curWorkspace.uri.fsPath; 12 | const rootManifest = path.join(workspaceRoot, 'Cargo.toml'); 13 | if (fs.existsSync(rootManifest)) { 14 | return curWorkspace; 15 | } 16 | 17 | // algorithm that will strip one folder at a time and check if that folder contains "Cargo.toml" 18 | let current = filePath; 19 | // eslint-disable-next-line no-constant-condition 20 | while (true) { 21 | const old = current; 22 | current = path.dirname(current); 23 | 24 | // break in case there is a bug that could result in a busy loop 25 | if (old === current) { 26 | break; 27 | } 28 | 29 | // break in case the strip folder reached the workspace root 30 | if (workspaceRoot === current) { 31 | break; 32 | } 33 | 34 | // check if "Cargo.toml" is present in the parent folder 35 | const cargoPath = path.join(current, 'Cargo.toml'); 36 | if (fs.existsSync(cargoPath)) { 37 | // ghetto change the uri on Workspace folder to make vscode think it's located elsewhere 38 | return { 39 | ...curWorkspace, 40 | name: path.basename(current), 41 | uri: Uri.file(current), 42 | }; 43 | } 44 | } 45 | 46 | return curWorkspace; 47 | } 48 | 49 | export function getOuterMostWorkspaceFolder( 50 | folder: WorkspaceFolder, 51 | ): WorkspaceFolder { 52 | const sortedFoldersByPrefix = (workspace.workspaceFolders || []) 53 | .map(folder => normalizeUriToPathPrefix(folder.uri)) 54 | .sort((a, b) => a.length - b.length); 55 | 56 | const uri = normalizeUriToPathPrefix(folder.uri); 57 | 58 | const outermostPath = sortedFoldersByPrefix.find(pre => uri.startsWith(pre)); 59 | return outermostPath 60 | ? workspace.getWorkspaceFolder(Uri.parse(outermostPath)) || folder 61 | : folder; 62 | } 63 | 64 | /** 65 | * Transforms a given URI to a path prefix, namely ensures that each path 66 | * segment ends with a path separator `/`. 67 | */ 68 | function normalizeUriToPathPrefix(uri: Uri): string { 69 | let result = uri.toString(); 70 | if (result.charAt(result.length - 1) !== '/') { 71 | result = result + '/'; 72 | } 73 | return result; 74 | } 75 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/snippets.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { assert } from './util'; 4 | 5 | export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) { 6 | assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`); 7 | const [uri, edits] = edit.entries()[0]; 8 | 9 | if (vscode.window.activeTextEditor?.document.uri !== uri) { 10 | // `vscode.window.visibleTextEditors` only contains editors whose contents are being displayed 11 | await vscode.window.showTextDocument(uri, {}); 12 | } 13 | const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString()); 14 | if (!editor) return; 15 | await applySnippetTextEdits(editor, edits); 16 | } 17 | 18 | export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) { 19 | let selection: vscode.Selection | undefined = undefined; 20 | let lineDelta = 0; 21 | await editor.edit((builder) => { 22 | for (const indel of edits) { 23 | const parsed = parseSnippet(indel.newText); 24 | if (parsed) { 25 | const [newText, [placeholderStart, placeholderLength]] = parsed; 26 | const prefix = newText.substr(0, placeholderStart); 27 | const lastNewline = prefix.lastIndexOf('\n'); 28 | 29 | const startLine = indel.range.start.line + lineDelta + countLines(prefix); 30 | const startColumn = lastNewline === -1 ? 31 | indel.range.start.character + placeholderStart 32 | : prefix.length - lastNewline - 1; 33 | const endColumn = startColumn + placeholderLength; 34 | selection = new vscode.Selection( 35 | new vscode.Position(startLine, startColumn), 36 | new vscode.Position(startLine, endColumn), 37 | ); 38 | builder.replace(indel.range, newText); 39 | } else { 40 | lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line); 41 | builder.replace(indel.range, indel.newText); 42 | } 43 | } 44 | }); 45 | if (selection) editor.selection = selection; 46 | } 47 | 48 | function parseSnippet(snip: string): [string, [number, number]] | undefined { 49 | const m = snip.match(/\$(0|\{0:([^}]*)\})/); 50 | if (!m) return undefined; 51 | const placeholder = m[2] ?? ""; 52 | const range: [number, number] = [m.index!!, placeholder.length]; 53 | const insert = snip.replace(m[0], placeholder); 54 | return [insert, range]; 55 | } 56 | 57 | function countLines(text: string): number { 58 | return (text.match(/\n/g) || []).length; 59 | } 60 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repo provides the RLS client for vscode built using the Language 4 | Server protocol. 5 | 6 | This file contains information for building, running, and debugging the plugin. 7 | If you just want to use it, you can download it from the VSCode marketplace. See 8 | [README.md](README.md) for more info. 9 | 10 | ## Building and Running 11 | 12 | Git clone or download the files and use `npm install` in the directory to 13 | download and install the required modules. 14 | 15 | Next, without installing the client as a regular VSCode extension, open the 16 | client folder in VSCode. Go to the debugger and run "Launch Extension". This 17 | opens a new instance of VSCode with the plugin installed. 18 | 19 | 20 | ### Via Rustup 21 | 22 | This is the default, if you don't set any of the environment variables below, 23 | the extension will run (and install) the RLS via rustup. You can install rustup 24 | from https://www.rustup.rs/. 25 | 26 | 27 | ### Via Source 28 | 29 | Check out the RLS source code, following the [directions](https://github.com/rust-lang-nursery/rls/blob/master/contributing.md). 30 | Point the `rust-client.rlsPath` setting at the RLS executable (e.g., 31 | `/rls/target/release/rls`). Note that you must include the name of the 32 | executable, not just the path. 33 | 34 | Note that this used to be possible via `rls.path` which is deprecated and `rls.root` 35 | which has been removed. 36 | 37 | ## Logging 38 | 39 | You can log to the output panel in VSCode by setting `rust-client.revealOutputChannelOn` to 40 | `info`. You can log to a file in the project directory by setting `rust-client.logToFile` 41 | to `true`. You won't see much logging unless you modify your RLS. 42 | 43 | 44 | ## Installing in VSCode 45 | 46 | If you'd like to test on multiple projects and already have the extension 47 | working properly, you can manually install the extension so that it's loaded 48 | into VSCode by default. To do so, run the following: 49 | 50 | ``` 51 | npm run installDevExtension 52 | ``` 53 | 54 | See the defenition of `installDevExtension` in `package.json` and [VSCode docs](https://code.visualstudio.com/Docs/extensions/example-hello-world#_installing-your-extension-locally) 55 | for more. 56 | 57 | 58 | ## Troubleshooting 59 | 60 | ### Error messages containing `tsc -watch -p ./` or `ENOSPC` 61 | 62 | ``` 63 | > npm ERR! Failed at the rls_vscode@0.0.1 compile script 'tsc -watch -p ./'. 64 | > npm ERR! Make sure you have the latest version of node.js and npm installed. 65 | > npm ERR! If you do, this is most likely a problem with the rls_vscode package, 66 | > npm ERR! not with npm itself. 67 | ``` 68 | 69 | run 70 | 71 | ``` 72 | > npm dedupe 73 | ``` 74 | 75 | see http://stackoverflow.com/a/31926452/1103681 for an explanation 76 | 77 | if that doesn't work, run 78 | 79 | ``` 80 | > echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p 81 | ``` 82 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/ctx.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as lc from 'vscode-languageclient'; 3 | import * as ra from './lsp_ext'; 4 | 5 | import { Config } from './config'; 6 | import { createClient } from './client'; 7 | import { isRustEditor, RustEditor } from './util'; 8 | import { Status } from './lsp_ext'; 9 | 10 | export class Ctx { 11 | private constructor( 12 | readonly config: Config, 13 | private readonly extCtx: vscode.ExtensionContext, 14 | readonly client: lc.LanguageClient, 15 | readonly serverPath: string, 16 | readonly statusBar: vscode.StatusBarItem, 17 | ) { 18 | 19 | } 20 | 21 | static async create( 22 | config: Config, 23 | extCtx: vscode.ExtensionContext, 24 | serverPath: string, 25 | cwd: string, 26 | ): Promise { 27 | const client = createClient(serverPath, cwd); 28 | 29 | const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); 30 | extCtx.subscriptions.push(statusBar); 31 | statusBar.text = "rust-analyzer"; 32 | statusBar.tooltip = "ready"; 33 | statusBar.show(); 34 | 35 | const res = new Ctx(config, extCtx, client, serverPath, statusBar); 36 | 37 | res.pushCleanup(client.start()); 38 | await client.onReady(); 39 | client.onNotification(ra.status, (status) => res.setStatus(status)); 40 | return res; 41 | } 42 | 43 | get activeRustEditor(): RustEditor | undefined { 44 | const editor = vscode.window.activeTextEditor; 45 | return editor && isRustEditor(editor) 46 | ? editor 47 | : undefined; 48 | } 49 | 50 | get visibleRustEditors(): RustEditor[] { 51 | return vscode.window.visibleTextEditors.filter(isRustEditor); 52 | } 53 | 54 | registerCommand(name: string, factory: (ctx: Ctx) => Cmd) { 55 | const fullName = `rust-analyzer.${name}`; 56 | const cmd = factory(this); 57 | const d = vscode.commands.registerCommand(fullName, cmd); 58 | this.pushCleanup(d); 59 | } 60 | 61 | get globalState(): vscode.Memento { 62 | return this.extCtx.globalState; 63 | } 64 | 65 | get subscriptions(): Disposable[] { 66 | return this.extCtx.subscriptions; 67 | } 68 | 69 | setStatus(status: Status) { 70 | switch (status) { 71 | case "loading": 72 | this.statusBar.text = "$(sync~spin) rust-analyzer"; 73 | this.statusBar.tooltip = "Loading the project"; 74 | this.statusBar.command = undefined; 75 | this.statusBar.color = undefined; 76 | break; 77 | case "ready": 78 | this.statusBar.text = "rust-analyzer"; 79 | this.statusBar.tooltip = "Ready"; 80 | this.statusBar.command = undefined; 81 | this.statusBar.color = undefined; 82 | break; 83 | case "invalid": 84 | this.statusBar.text = "$(error) rust-analyzer"; 85 | this.statusBar.tooltip = "Failed to load the project"; 86 | this.statusBar.command = undefined; 87 | this.statusBar.color = new vscode.ThemeColor("notificationsErrorIcon.foreground"); 88 | break; 89 | case "needsReload": 90 | this.statusBar.text = "$(warning) rust-analyzer"; 91 | this.statusBar.tooltip = "Click to reload"; 92 | this.statusBar.command = "rust-analyzer.reloadWorkspace"; 93 | this.statusBar.color = new vscode.ThemeColor("notificationsWarningIcon.foreground"); 94 | break; 95 | } 96 | } 97 | 98 | pushCleanup(d: Disposable) { 99 | this.extCtx.subscriptions.push(d); 100 | } 101 | } 102 | 103 | export interface Disposable { 104 | dispose(): void; 105 | } 106 | export type Cmd = (...args: any[]) => unknown; 107 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/tests/unit/runnable_env.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { prepareEnv } from '../../src/run'; 3 | import { RunnableEnvCfg } from '../../src/config'; 4 | import * as ra from '../../src/lsp_ext'; 5 | 6 | function makeRunnable(label: string): ra.Runnable { 7 | return { 8 | label, 9 | kind: "cargo", 10 | args: { 11 | cargoArgs: [], 12 | executableArgs: [] 13 | } 14 | }; 15 | } 16 | 17 | function fakePrepareEnv(runnableName: string, config: RunnableEnvCfg): Record { 18 | const runnable = makeRunnable(runnableName); 19 | return prepareEnv(runnable, config); 20 | } 21 | 22 | suite('Runnable env', () => { 23 | test('Global config works', () => { 24 | const binEnv = fakePrepareEnv("run project_name", { "GLOBAL": "g" }); 25 | assert.equal(binEnv["GLOBAL"], "g"); 26 | 27 | const testEnv = fakePrepareEnv("test some::mod::test_name", { "GLOBAL": "g" }); 28 | assert.equal(testEnv["GLOBAL"], "g"); 29 | }); 30 | 31 | test('null mask works', () => { 32 | const config = [ 33 | { 34 | env: { DATA: "data" } 35 | } 36 | ]; 37 | const binEnv = fakePrepareEnv("run project_name", config); 38 | assert.equal(binEnv["DATA"], "data"); 39 | 40 | const testEnv = fakePrepareEnv("test some::mod::test_name", config); 41 | assert.equal(testEnv["DATA"], "data"); 42 | }); 43 | 44 | test('order works', () => { 45 | const config = [ 46 | { 47 | env: { DATA: "data" } 48 | }, 49 | { 50 | env: { DATA: "newdata" } 51 | } 52 | ]; 53 | const binEnv = fakePrepareEnv("run project_name", config); 54 | assert.equal(binEnv["DATA"], "newdata"); 55 | 56 | const testEnv = fakePrepareEnv("test some::mod::test_name", config); 57 | assert.equal(testEnv["DATA"], "newdata"); 58 | }); 59 | 60 | test('mask works', () => { 61 | const config = [ 62 | { 63 | env: { DATA: "data" } 64 | }, 65 | { 66 | mask: "^run", 67 | env: { DATA: "rundata" } 68 | }, 69 | { 70 | mask: "special_test$", 71 | env: { DATA: "special_test" } 72 | } 73 | ]; 74 | const binEnv = fakePrepareEnv("run project_name", config); 75 | assert.equal(binEnv["DATA"], "rundata"); 76 | 77 | const testEnv = fakePrepareEnv("test some::mod::test_name", config); 78 | assert.equal(testEnv["DATA"], "data"); 79 | 80 | const specialTestEnv = fakePrepareEnv("test some::mod::special_test", config); 81 | assert.equal(specialTestEnv["DATA"], "special_test"); 82 | }); 83 | 84 | test('exact test name works', () => { 85 | const config = [ 86 | { 87 | env: { DATA: "data" } 88 | }, 89 | { 90 | mask: "some::mod::test_name", 91 | env: { DATA: "test special" } 92 | } 93 | ]; 94 | const testEnv = fakePrepareEnv("test some::mod::test_name", config); 95 | assert.equal(testEnv["DATA"], "test special"); 96 | 97 | const specialTestEnv = fakePrepareEnv("test some::mod::another_test", config); 98 | assert.equal(specialTestEnv["DATA"], "data"); 99 | }); 100 | 101 | test('test mod name works', () => { 102 | const config = [ 103 | { 104 | env: { DATA: "data" } 105 | }, 106 | { 107 | mask: "some::mod", 108 | env: { DATA: "mod special" } 109 | } 110 | ]; 111 | const testEnv = fakePrepareEnv("test some::mod::test_name", config); 112 | assert.equal(testEnv["DATA"], "mod special"); 113 | 114 | const specialTestEnv = fakePrepareEnv("test some::mod::another_test", config); 115 | assert.equal(specialTestEnv["DATA"], "mod special"); 116 | }); 117 | 118 | }); 119 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/lsp_ext.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file mirrors `crates/rust-analyzer/src/req.rs` declarations. 3 | */ 4 | 5 | import * as lc from "vscode-languageclient"; 6 | 7 | export const analyzerStatus = new lc.RequestType("rust-analyzer/analyzerStatus"); 8 | export const memoryUsage = new lc.RequestType("rust-analyzer/memoryUsage"); 9 | 10 | export type Status = "loading" | "ready" | "invalid" | "needsReload"; 11 | export const status = new lc.NotificationType("rust-analyzer/status"); 12 | 13 | export const reloadWorkspace = new lc.RequestType("rust-analyzer/reloadWorkspace"); 14 | 15 | export interface SyntaxTreeParams { 16 | textDocument: lc.TextDocumentIdentifier; 17 | range: lc.Range | null; 18 | } 19 | export const syntaxTree = new lc.RequestType("rust-analyzer/syntaxTree"); 20 | 21 | 22 | export interface ExpandMacroParams { 23 | textDocument: lc.TextDocumentIdentifier; 24 | position: lc.Position; 25 | } 26 | export interface ExpandedMacro { 27 | name: string; 28 | expansion: string; 29 | } 30 | export const expandMacro = new lc.RequestType("rust-analyzer/expandMacro"); 31 | 32 | export interface MatchingBraceParams { 33 | textDocument: lc.TextDocumentIdentifier; 34 | positions: lc.Position[]; 35 | } 36 | export const matchingBrace = new lc.RequestType("experimental/matchingBrace"); 37 | 38 | export const parentModule = new lc.RequestType("experimental/parentModule"); 39 | 40 | export interface ResolveCodeActionParams { 41 | id: string; 42 | codeActionParams: lc.CodeActionParams; 43 | } 44 | export const resolveCodeAction = new lc.RequestType('experimental/resolveCodeAction'); 45 | 46 | export interface JoinLinesParams { 47 | textDocument: lc.TextDocumentIdentifier; 48 | ranges: lc.Range[]; 49 | } 50 | export const joinLines = new lc.RequestType("experimental/joinLines"); 51 | 52 | export const onEnter = new lc.RequestType("experimental/onEnter"); 53 | 54 | export interface RunnablesParams { 55 | textDocument: lc.TextDocumentIdentifier; 56 | position: lc.Position | null; 57 | } 58 | 59 | export interface Runnable { 60 | label: string; 61 | location?: lc.LocationLink; 62 | kind: "cargo"; 63 | args: { 64 | workspaceRoot?: string; 65 | cargoArgs: string[]; 66 | executableArgs: string[]; 67 | expectTest?: boolean; 68 | }; 69 | } 70 | export const runnables = new lc.RequestType("experimental/runnables"); 71 | 72 | export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint; 73 | 74 | export namespace InlayHint { 75 | export const enum Kind { 76 | TypeHint = "TypeHint", 77 | ParamHint = "ParameterHint", 78 | ChainingHint = "ChainingHint", 79 | } 80 | interface Common { 81 | range: lc.Range; 82 | label: string; 83 | } 84 | export type TypeHint = Common & { kind: Kind.TypeHint }; 85 | export type ParamHint = Common & { kind: Kind.ParamHint }; 86 | export type ChainingHint = Common & { kind: Kind.ChainingHint }; 87 | } 88 | export interface InlayHintsParams { 89 | textDocument: lc.TextDocumentIdentifier; 90 | } 91 | export const inlayHints = new lc.RequestType("rust-analyzer/inlayHints"); 92 | 93 | export interface SsrParams { 94 | query: string; 95 | parseOnly: boolean; 96 | } 97 | export const ssr = new lc.RequestType('experimental/ssr'); 98 | 99 | export interface CommandLink extends lc.Command { 100 | /** 101 | * A tooltip for the command, when represented in the UI. 102 | */ 103 | tooltip?: string; 104 | } 105 | 106 | export interface CommandLinkGroup { 107 | title?: string; 108 | commands: CommandLink[]; 109 | } 110 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import { workspace, WorkspaceConfiguration } from 'vscode'; 2 | import { RevealOutputChannelOn } from 'vscode-languageclient'; 3 | 4 | import { getActiveChannel, RustupConfig } from './rustup'; 5 | 6 | function fromStringToRevealOutputChannelOn( 7 | value: string, 8 | ): RevealOutputChannelOn { 9 | switch (value && value.toLowerCase()) { 10 | case 'info': 11 | return RevealOutputChannelOn.Info; 12 | case 'warn': 13 | return RevealOutputChannelOn.Warn; 14 | case 'error': 15 | return RevealOutputChannelOn.Error; 16 | case 'never': 17 | default: 18 | return RevealOutputChannelOn.Never; 19 | } 20 | } 21 | 22 | export class RLSConfiguration { 23 | private readonly configuration: WorkspaceConfiguration; 24 | private readonly wsPath: string; 25 | 26 | private constructor(configuration: WorkspaceConfiguration, wsPath: string) { 27 | this.configuration = configuration; 28 | this.wsPath = wsPath; 29 | } 30 | 31 | public static loadFromWorkspace(wsPath: string): RLSConfiguration { 32 | const configuration = workspace.getConfiguration(); 33 | return new RLSConfiguration(configuration, wsPath); 34 | } 35 | 36 | private static readRevealOutputChannelOn( 37 | configuration: WorkspaceConfiguration, 38 | ) { 39 | const setting = configuration.get( 40 | 'rust-client.revealOutputChannelOn', 41 | 'never', 42 | ); 43 | return fromStringToRevealOutputChannelOn(setting); 44 | } 45 | 46 | /** 47 | * Tries to fetch the `rust-client.channel` configuration value. If missing, 48 | * falls back on active toolchain specified by rustup (at `rustupPath`), 49 | * finally defaulting to `nightly` if all fails. 50 | */ 51 | private static readChannel( 52 | wsPath: string, 53 | rustupPath: string, 54 | configuration: WorkspaceConfiguration, 55 | ): string { 56 | const channel = configuration.get('rust-client.channel'); 57 | if (channel === 'default' || !channel) { 58 | try { 59 | return getActiveChannel(wsPath, rustupPath); 60 | } catch (e) { 61 | // rustup might not be installed at the time the configuration is 62 | // initially loaded, so silently ignore the error and return a default value 63 | return 'nightly'; 64 | } 65 | } else { 66 | return channel; 67 | } 68 | } 69 | 70 | public get rustupPath(): string { 71 | return this.configuration.get('rust-client.rustupPath', 'rustup'); 72 | } 73 | 74 | public get logToFile(): boolean { 75 | return this.configuration.get('rust-client.logToFile', false); 76 | } 77 | 78 | public get rustupDisabled(): boolean { 79 | const rlsOverriden = Boolean(this.rlsPath); 80 | return ( 81 | rlsOverriden || 82 | this.configuration.get('rust-client.disableRustup', false) 83 | ); 84 | } 85 | 86 | public get rustAnalyzer(): { path?: string; releaseTag: string } { 87 | const cfg = this.configuration; 88 | const releaseTag = cfg.get('rust.rust-analyzer.releaseTag', 'nightly'); 89 | const path = cfg.get('rust.rust-analyzer.path'); 90 | return { releaseTag, ...{ path } }; 91 | } 92 | 93 | public get revealOutputChannelOn(): RevealOutputChannelOn { 94 | return RLSConfiguration.readRevealOutputChannelOn(this.configuration); 95 | } 96 | 97 | public get updateOnStartup(): boolean { 98 | return this.configuration.get('rust-client.updateOnStartup', true); 99 | } 100 | 101 | public get channel(): string { 102 | return RLSConfiguration.readChannel( 103 | this.wsPath, 104 | this.rustupPath, 105 | this.configuration, 106 | ); 107 | } 108 | 109 | /** 110 | * If specified, RLS will be spawned by executing a file at the given path. 111 | */ 112 | public get rlsPath(): string | undefined { 113 | return this.configuration.get('rust-client.rlsPath'); 114 | } 115 | 116 | /** Returns the language analysis engine to be used for the workspace */ 117 | public get engine(): 'rls' | 'rust-analyzer' { 118 | return this.configuration.get('rust-client.engine') || 'rls'; 119 | } 120 | 121 | /** 122 | * Whether a language server should be automatically started when opening 123 | * a relevant Rust project. 124 | */ 125 | public get autoStartRls(): boolean { 126 | return this.configuration.get('rust-client.autoStartRls', true); 127 | } 128 | 129 | public rustupConfig(): RustupConfig { 130 | return { 131 | channel: this.channel, 132 | path: this.rustupPath, 133 | }; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/tasks.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as toolchain from "./toolchain"; 3 | import { Config } from './config'; 4 | import { log } from './util'; 5 | 6 | // This ends up as the `type` key in tasks.json. RLS also uses `cargo` and 7 | // our configuration should be compatible with it so use the same key. 8 | export const TASK_TYPE = 'cargo'; 9 | export const TASK_SOURCE = 'rust'; 10 | 11 | export interface CargoTaskDefinition extends vscode.TaskDefinition { 12 | command?: string; 13 | args?: string[]; 14 | cwd?: string; 15 | env?: { [key: string]: string }; 16 | } 17 | 18 | class CargoTaskProvider implements vscode.TaskProvider { 19 | private readonly target: vscode.WorkspaceFolder; 20 | private readonly config: Config; 21 | 22 | constructor(target: vscode.WorkspaceFolder, config: Config) { 23 | this.target = target; 24 | this.config = config; 25 | } 26 | 27 | async provideTasks(): Promise { 28 | // Detect Rust tasks. Currently we do not do any actual detection 29 | // of tasks (e.g. aliases in .cargo/config) and just return a fixed 30 | // set of tasks that always exist. These tasks cannot be removed in 31 | // tasks.json - only tweaked. 32 | 33 | const defs = [ 34 | { command: 'build', group: vscode.TaskGroup.Build }, 35 | { command: 'check', group: vscode.TaskGroup.Build }, 36 | { command: 'test', group: vscode.TaskGroup.Test }, 37 | { command: 'clean', group: vscode.TaskGroup.Clean }, 38 | { command: 'run', group: undefined }, 39 | ]; 40 | 41 | const tasks: vscode.Task[] = []; 42 | for (const def of defs) { 43 | const vscodeTask = await buildCargoTask(this.target, { type: TASK_TYPE, command: def.command }, `cargo ${def.command}`, [def.command], this.config.cargoRunner); 44 | vscodeTask.group = def.group; 45 | tasks.push(vscodeTask); 46 | } 47 | 48 | return tasks; 49 | } 50 | 51 | async resolveTask(task: vscode.Task): Promise { 52 | // VSCode calls this for every cargo task in the user's tasks.json, 53 | // we need to inform VSCode how to execute that command by creating 54 | // a ShellExecution for it. 55 | 56 | const definition = task.definition as CargoTaskDefinition; 57 | 58 | if (definition.type === TASK_TYPE && definition.command) { 59 | const args = [definition.command].concat(definition.args ?? []); 60 | 61 | return await buildCargoTask(this.target, definition, task.name, args, this.config.cargoRunner); 62 | } 63 | 64 | return undefined; 65 | } 66 | } 67 | 68 | export async function buildCargoTask( 69 | target: vscode.WorkspaceFolder, 70 | definition: CargoTaskDefinition, 71 | name: string, 72 | args: string[], 73 | customRunner?: string, 74 | throwOnError: boolean = false 75 | ): Promise { 76 | 77 | let exec: vscode.ShellExecution | undefined = undefined; 78 | 79 | if (customRunner) { 80 | const runnerCommand = `${customRunner}.buildShellExecution`; 81 | try { 82 | const runnerArgs = { kind: TASK_TYPE, args, cwd: definition.cwd, env: definition.env }; 83 | const customExec = await vscode.commands.executeCommand(runnerCommand, runnerArgs); 84 | if (customExec) { 85 | if (customExec instanceof vscode.ShellExecution) { 86 | exec = customExec; 87 | } else { 88 | log.debug("Invalid cargo ShellExecution", customExec); 89 | throw "Invalid cargo ShellExecution."; 90 | } 91 | } 92 | // fallback to default processing 93 | 94 | } catch (e) { 95 | if (throwOnError) throw `Cargo runner '${customRunner}' failed! ${e}`; 96 | // fallback to default processing 97 | } 98 | } 99 | 100 | if (!exec) { 101 | exec = new vscode.ShellExecution(toolchain.cargoPath(), args, definition); 102 | } 103 | 104 | return new vscode.Task( 105 | definition, 106 | target, 107 | name, 108 | TASK_SOURCE, 109 | exec, 110 | ['$rustc'] 111 | ); 112 | } 113 | 114 | export function activateTaskProvider(target: vscode.WorkspaceFolder, config: Config): vscode.Disposable { 115 | const provider = new CargoTaskProvider(target, config); 116 | return vscode.tasks.registerTaskProvider(TASK_TYPE, provider); 117 | } 118 | -------------------------------------------------------------------------------- /src/net.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as fs from 'fs'; 3 | import fetch from 'node-fetch'; 4 | import * as stream from 'stream'; 5 | import * as util from 'util'; 6 | import * as vscode from 'vscode'; 7 | 8 | const pipeline = util.promisify(stream.pipeline); 9 | 10 | const GITHUB_API_ENDPOINT_URL = 'https://api.github.com'; 11 | 12 | export async function fetchRelease( 13 | owner: string, 14 | repository: string, 15 | releaseTag: string, 16 | ): Promise { 17 | const apiEndpointPath = `/repos/${owner}/${repository}/releases/tags/${releaseTag}`; 18 | 19 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; 20 | 21 | console.debug( 22 | 'Issuing request for released artifacts metadata to', 23 | requestUrl, 24 | ); 25 | 26 | const response = await fetch(requestUrl, { 27 | headers: { Accept: 'application/vnd.github.v3+json' }, 28 | }); 29 | 30 | if (!response.ok) { 31 | console.error('Error fetching artifact release info', { 32 | requestUrl, 33 | releaseTag, 34 | response: { 35 | headers: response.headers, 36 | status: response.status, 37 | body: await response.text(), 38 | }, 39 | }); 40 | 41 | throw new Error( 42 | `Got response ${response.status} when trying to fetch ` + 43 | `release info for ${releaseTag} release`, 44 | ); 45 | } 46 | 47 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) 48 | const release: GithubRelease = await response.json(); 49 | return release; 50 | } 51 | 52 | // We omit declaration of tremendous amount of fields that we are not using here 53 | export interface GithubRelease { 54 | name: string; 55 | id: number; 56 | // eslint-disable-next-line camelcase 57 | published_at: string; 58 | assets: Array<{ 59 | name: string; 60 | // eslint-disable-next-line camelcase 61 | browser_download_url: string; 62 | }>; 63 | } 64 | 65 | export async function download( 66 | downloadUrl: string, 67 | destinationPath: string, 68 | progressTitle: string, 69 | { mode }: { mode?: number } = {}, 70 | ) { 71 | await vscode.window.withProgress( 72 | { 73 | location: vscode.ProgressLocation.Notification, 74 | cancellable: false, 75 | title: progressTitle, 76 | }, 77 | async (progress, _cancellationToken) => { 78 | let lastPercentage = 0; 79 | await downloadFile( 80 | downloadUrl, 81 | destinationPath, 82 | mode, 83 | (readBytes, totalBytes) => { 84 | const newPercentage = (readBytes / totalBytes) * 100; 85 | progress.report({ 86 | message: newPercentage.toFixed(0) + '%', 87 | increment: newPercentage - lastPercentage, 88 | }); 89 | 90 | lastPercentage = newPercentage; 91 | }, 92 | ); 93 | }, 94 | ); 95 | } 96 | 97 | /** 98 | * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. 99 | * `onProgress` callback is called on recieveing each chunk of bytes 100 | * to track the progress of downloading, it gets the already read and total 101 | * amount of bytes to read as its parameters. 102 | */ 103 | async function downloadFile( 104 | url: string, 105 | destFilePath: fs.PathLike, 106 | mode: number | undefined, 107 | onProgress: (readBytes: number, totalBytes: number) => void, 108 | ): Promise { 109 | const res = await fetch(url); 110 | 111 | if (!res.ok) { 112 | console.error('Error', res.status, 'while downloading file from', url); 113 | console.error({ body: await res.text(), headers: res.headers }); 114 | 115 | throw new Error( 116 | `Got response ${res.status} when trying to download a file.`, 117 | ); 118 | } 119 | 120 | const totalBytes = Number(res.headers.get('content-length')); 121 | assert(!Number.isNaN(totalBytes), 'Sanity check of content-length protocol'); 122 | 123 | console.debug( 124 | 'Downloading file of', 125 | totalBytes, 126 | 'bytes size from', 127 | url, 128 | 'to', 129 | destFilePath, 130 | ); 131 | 132 | let readBytes = 0; 133 | res.body.on('data', (chunk: Buffer) => { 134 | readBytes += chunk.length; 135 | onProgress(readBytes, totalBytes); 136 | }); 137 | 138 | const destFileStream = fs.createWriteStream(destFilePath, { mode }); 139 | 140 | await pipeline(res.body, destFileStream); 141 | return new Promise(resolve => { 142 | destFileStream.on('close', resolve); 143 | destFileStream.destroy(); 144 | 145 | // Details on workaround: https://github.com/rust-analyzer/rust-analyzer/pull/3092#discussion_r378191131 146 | // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 147 | }); 148 | } 149 | -------------------------------------------------------------------------------- /test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as path from 'path'; 3 | import * as vscode from 'vscode'; 4 | import { Disposable, Uri } from 'vscode'; 5 | 6 | import * as extension from '../../src/extension'; 7 | import { Observable } from '../../src/utils/observable'; 8 | 9 | const fixtureDir = path.resolve( 10 | path.join(__dirname, '..', '..', '..', 'fixtures'), 11 | ); 12 | 13 | suite('Extension Tests', () => { 14 | test('cargo tasks are auto-detected', async () => { 15 | // Activate manually to ease the access to internal, exported APIs without 16 | // having to open any file before 17 | const ext = vscode.extensions.getExtension( 18 | 'rust-lang.rust', 19 | )!; 20 | const { activeWorkspace } = await ext.activate(); 21 | 22 | const projects = [ 23 | path.join(fixtureDir, 'bare-lib-project'), 24 | path.join(fixtureDir, 'another-lib-project'), 25 | ].map(path => Uri.file(path).fsPath); 26 | 27 | const expected = [ 28 | { subcommand: 'build', group: vscode.TaskGroup.Build, cwd: projects[0] }, 29 | { subcommand: 'build', group: vscode.TaskGroup.Build, cwd: projects[1] }, 30 | { subcommand: 'check', group: vscode.TaskGroup.Build, cwd: projects[0] }, 31 | { subcommand: 'check', group: vscode.TaskGroup.Build, cwd: projects[1] }, 32 | { subcommand: 'test', group: vscode.TaskGroup.Test, cwd: projects[1] }, 33 | { subcommand: 'clean', group: vscode.TaskGroup.Clean, cwd: projects[1] }, 34 | { subcommand: 'run', group: undefined, cwd: projects[1] }, 35 | ]; 36 | 37 | const whenWorkspacesActive = projects.map(path => 38 | whenWorkspaceActive(activeWorkspace, path), 39 | ); 40 | 41 | // This makes sure that we set the focus on the opened files (which is what 42 | // actually triggers the extension for the project) 43 | await vscode.commands.executeCommand( 44 | 'workbench.action.quickOpen', 45 | path.join(projects[0], 'src', 'lib.rs'), 46 | ); 47 | await waitForUI(); 48 | await vscode.commands.executeCommand( 49 | 'workbench.action.acceptSelectedQuickOpenItem', 50 | ); 51 | await waitForUI(); 52 | await vscode.commands.executeCommand('workbench.action.keepEditor'); 53 | // Wait until the first server is ready 54 | await whenWorkspacesActive[0]; 55 | 56 | expect(await fetchBriefTasks()).to.include.deep.members([expected[0]]); 57 | 58 | // Now test for the second project 59 | await vscode.commands.executeCommand( 60 | 'workbench.action.quickOpen', 61 | path.join(projects[1], 'src', 'lib.rs'), 62 | ); 63 | await waitForUI(); 64 | await vscode.commands.executeCommand( 65 | 'workbench.action.acceptSelectedQuickOpenItem', 66 | ); 67 | await waitForUI(); 68 | // Wait until the second server is ready 69 | await whenWorkspacesActive[1]; 70 | expect(await fetchBriefTasks()).to.include.deep.members(expected); 71 | }).timeout(60000); 72 | }); 73 | 74 | /** Fetches current VSCode tasks' partial objects for ease of assertion */ 75 | async function fetchBriefTasks(): Promise< 76 | Array<{ 77 | subcommand: string; 78 | group: vscode.TaskGroup | undefined; 79 | cwd?: string; 80 | }> 81 | > { 82 | const tasks = await vscode.tasks.fetchTasks(); 83 | 84 | return tasks.map(task => ({ 85 | subcommand: task.definition.subcommand, 86 | group: task.group, 87 | cwd: 88 | ((task.execution instanceof vscode.ProcessExecution || 89 | task.execution instanceof vscode.ShellExecution) && 90 | task.execution?.options?.cwd) || 91 | undefined, 92 | })); 93 | } 94 | 95 | /** 96 | * Returns a promise when a client workspace will become active with a given path. 97 | * @param fsPath normalized file system path of a URI 98 | */ 99 | function whenWorkspaceActive( 100 | observable: Observable, 101 | fsPath: string, 102 | ): Promise { 103 | return new Promise(resolve => { 104 | let disposable: Disposable | undefined; 105 | disposable = observable.observe(value => { 106 | if (value && value.folder.uri.fsPath === fsPath) { 107 | if (disposable) { 108 | disposable.dispose(); 109 | disposable = undefined; 110 | } 111 | 112 | resolve(value); 113 | } 114 | }); 115 | }); 116 | } 117 | 118 | const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 119 | /** 120 | * Returns a promise that resolves after executing the current call stack. 121 | * Sometimes we need to wait for the UI to catch up (since it's executed on the 122 | * same thread as JS) before and UI-dependent logic, such as a sequence of 123 | * user-like inputs. To avoid any races (which unfortunately *did* happen), it's 124 | * best if we interweave the delays between each UI action. 125 | */ 126 | // FIXME: ... or just use 500ms? For some reason our CI just can't ever catch up 127 | const waitForUI = () => delay(process.env.CI === 'true' ? 500 : 0); 128 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/util.ts: -------------------------------------------------------------------------------- 1 | import * as lc from "vscode-languageclient"; 2 | import * as vscode from "vscode"; 3 | import { strict as nativeAssert } from "assert"; 4 | import { spawnSync } from "child_process"; 5 | import { inspect } from "util"; 6 | 7 | export function assert(condition: boolean, explanation: string): asserts condition { 8 | try { 9 | nativeAssert(condition, explanation); 10 | } catch (err) { 11 | log.error(`Assertion failed:`, explanation); 12 | throw err; 13 | } 14 | } 15 | 16 | export const log = new class { 17 | private enabled = true; 18 | private readonly output = vscode.window.createOutputChannel("Rust Analyzer Client"); 19 | 20 | setEnabled(yes: boolean): void { 21 | log.enabled = yes; 22 | } 23 | 24 | // Hint: the type [T, ...T[]] means a non-empty array 25 | debug(...msg: [unknown, ...unknown[]]): void { 26 | if (!log.enabled) return; 27 | log.write("DEBUG", ...msg); 28 | log.output.toString(); 29 | } 30 | 31 | info(...msg: [unknown, ...unknown[]]): void { 32 | log.write("INFO", ...msg); 33 | } 34 | 35 | warn(...msg: [unknown, ...unknown[]]): void { 36 | debugger; 37 | log.write("WARN", ...msg); 38 | } 39 | 40 | error(...msg: [unknown, ...unknown[]]): void { 41 | debugger; 42 | log.write("ERROR", ...msg); 43 | log.output.show(true); 44 | } 45 | 46 | private write(label: string, ...messageParts: unknown[]): void { 47 | const message = messageParts.map(log.stringify).join(" "); 48 | const dateTime = new Date().toLocaleString(); 49 | log.output.appendLine(`${label} [${dateTime}]: ${message}`); 50 | } 51 | 52 | private stringify(val: unknown): string { 53 | if (typeof val === "string") return val; 54 | return inspect(val, { 55 | colors: false, 56 | depth: 6, // heuristic 57 | }); 58 | } 59 | }; 60 | 61 | export async function sendRequestWithRetry( 62 | client: lc.LanguageClient, 63 | reqType: lc.RequestType, 64 | param: TParam, 65 | token?: vscode.CancellationToken, 66 | ): Promise { 67 | for (const delay of [2, 4, 6, 8, 10, null]) { 68 | try { 69 | return await (token 70 | ? client.sendRequest(reqType, param, token) 71 | : client.sendRequest(reqType, param) 72 | ); 73 | } catch (error) { 74 | if (delay === null) { 75 | log.warn("LSP request timed out", { method: reqType.method, param, error }); 76 | throw error; 77 | } 78 | 79 | if (error.code === lc.ErrorCodes.RequestCancelled) { 80 | throw error; 81 | } 82 | 83 | if (error.code !== lc.ErrorCodes.ContentModified) { 84 | log.warn("LSP request failed", { method: reqType.method, param, error }); 85 | throw error; 86 | } 87 | 88 | await sleep(10 * (1 << delay)); 89 | } 90 | } 91 | throw 'unreachable'; 92 | } 93 | 94 | export function sleep(ms: number) { 95 | return new Promise(resolve => setTimeout(resolve, ms)); 96 | } 97 | 98 | export type RustDocument = vscode.TextDocument & { languageId: "rust" }; 99 | export type RustEditor = vscode.TextEditor & { document: RustDocument }; 100 | 101 | export function isRustDocument(document: vscode.TextDocument): document is RustDocument { 102 | // Prevent corrupted text (particularly via inlay hints) in diff views 103 | // by allowing only `file` schemes 104 | // unfortunately extensions that use diff views not always set this 105 | // to something different than 'file' (see ongoing bug: #4608) 106 | return document.languageId === 'rust' && document.uri.scheme === 'file'; 107 | } 108 | 109 | export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { 110 | return isRustDocument(editor.document); 111 | } 112 | 113 | export function isValidExecutable(path: string): boolean { 114 | log.debug("Checking availability of a binary at", path); 115 | 116 | const res = spawnSync(path, ["--version"], { encoding: 'utf8' }); 117 | 118 | const printOutput = res.error && (res.error as any).code !== 'ENOENT' ? log.warn : log.debug; 119 | printOutput(path, "--version:", res); 120 | 121 | return res.status === 0; 122 | } 123 | 124 | /** Sets ['when'](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts) clause contexts */ 125 | export function setContextValue(key: string, value: any): Thenable { 126 | return vscode.commands.executeCommand('setContext', key, value); 127 | } 128 | 129 | /** 130 | * Returns a higher-order function that caches the results of invoking the 131 | * underlying function. 132 | */ 133 | export function memoize(func: (this: TThis, arg: Param) => Ret) { 134 | const cache = new Map(); 135 | 136 | return function(this: TThis, arg: Param) { 137 | const cached = cache.get(arg); 138 | if (cached) return cached; 139 | 140 | const result = func.call(this, arg); 141 | cache.set(arg, result); 142 | 143 | return result; 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/net.ts: -------------------------------------------------------------------------------- 1 | // Replace with `import fetch from "node-fetch"` once this is fixed in rollup: 2 | // https://github.com/rollup/plugins/issues/491 3 | const fetch = require("node-fetch") as typeof import("node-fetch")["default"]; 4 | 5 | import * as vscode from "vscode"; 6 | import * as stream from "stream"; 7 | import * as crypto from "crypto"; 8 | import * as fs from "fs"; 9 | import * as zlib from "zlib"; 10 | import * as util from "util"; 11 | import * as path from "path"; 12 | import { log, assert } from "./util"; 13 | 14 | const pipeline = util.promisify(stream.pipeline); 15 | 16 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; 17 | const OWNER = "rust-analyzer"; 18 | const REPO = "rust-analyzer"; 19 | 20 | export async function fetchRelease( 21 | releaseTag: string 22 | ): Promise { 23 | 24 | const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`; 25 | 26 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; 27 | 28 | log.debug("Issuing request for released artifacts metadata to", requestUrl); 29 | 30 | const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } }); 31 | 32 | if (!response.ok) { 33 | log.error("Error fetching artifact release info", { 34 | requestUrl, 35 | releaseTag, 36 | response: { 37 | headers: response.headers, 38 | status: response.status, 39 | body: await response.text(), 40 | } 41 | }); 42 | 43 | throw new Error( 44 | `Got response ${response.status} when trying to fetch ` + 45 | `release info for ${releaseTag} release` 46 | ); 47 | } 48 | 49 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) 50 | const release: GithubRelease = await response.json(); 51 | return release; 52 | } 53 | 54 | // We omit declaration of tremendous amount of fields that we are not using here 55 | export interface GithubRelease { 56 | name: string; 57 | id: number; 58 | // eslint-disable-next-line camelcase 59 | published_at: string; 60 | assets: Array<{ 61 | name: string; 62 | // eslint-disable-next-line camelcase 63 | browser_download_url: string; 64 | }>; 65 | } 66 | 67 | interface DownloadOpts { 68 | progressTitle: string; 69 | url: string; 70 | dest: string; 71 | mode?: number; 72 | gunzip?: boolean; 73 | } 74 | 75 | export async function download(opts: DownloadOpts) { 76 | // Put artifact into a temporary file (in the same dir for simplicity) 77 | // to prevent partially downloaded files when user kills vscode 78 | const dest = path.parse(opts.dest); 79 | const randomHex = crypto.randomBytes(5).toString("hex"); 80 | const tempFile = path.join(dest.dir, `${dest.name}${randomHex}`); 81 | 82 | await vscode.window.withProgress( 83 | { 84 | location: vscode.ProgressLocation.Notification, 85 | cancellable: false, 86 | title: opts.progressTitle 87 | }, 88 | async (progress, _cancellationToken) => { 89 | let lastPercentage = 0; 90 | await downloadFile(opts.url, tempFile, opts.mode, !!opts.gunzip, (readBytes, totalBytes) => { 91 | const newPercentage = (readBytes / totalBytes) * 100; 92 | progress.report({ 93 | message: newPercentage.toFixed(0) + "%", 94 | increment: newPercentage - lastPercentage 95 | }); 96 | 97 | lastPercentage = newPercentage; 98 | }); 99 | } 100 | ); 101 | 102 | await fs.promises.rename(tempFile, opts.dest); 103 | } 104 | 105 | async function downloadFile( 106 | url: string, 107 | destFilePath: fs.PathLike, 108 | mode: number | undefined, 109 | gunzip: boolean, 110 | onProgress: (readBytes: number, totalBytes: number) => void 111 | ): Promise { 112 | const res = await fetch(url); 113 | 114 | if (!res.ok) { 115 | log.error("Error", res.status, "while downloading file from", url); 116 | log.error({ body: await res.text(), headers: res.headers }); 117 | 118 | throw new Error(`Got response ${res.status} when trying to download a file.`); 119 | } 120 | 121 | const totalBytes = Number(res.headers.get('content-length')); 122 | assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol"); 123 | 124 | log.debug("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath); 125 | 126 | let readBytes = 0; 127 | res.body.on("data", (chunk: Buffer) => { 128 | readBytes += chunk.length; 129 | onProgress(readBytes, totalBytes); 130 | }); 131 | 132 | const destFileStream = fs.createWriteStream(destFilePath, { mode }); 133 | const srcStream = gunzip ? res.body.pipe(zlib.createGunzip()) : res.body; 134 | 135 | await pipeline(srcStream, destFileStream); 136 | 137 | await new Promise(resolve => { 138 | destFileStream.on("close", resolve); 139 | destFileStream.destroy(); 140 | // This workaround is awaiting to be removed when vscode moves to newer nodejs version: 141 | // https://github.com/rust-analyzer/rust-analyzer/issues/3167 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /src/tasks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Disposable, 3 | ShellExecution, 4 | Task, 5 | TaskGroup, 6 | TaskProvider, 7 | tasks, 8 | workspace, 9 | WorkspaceFolder, 10 | } from 'vscode'; 11 | 12 | /** 13 | * Displayed identifier associated with each task. 14 | */ 15 | const TASK_SOURCE = 'Rust'; 16 | /** 17 | * Internal VSCode task type (namespace) under which extensions register their 18 | * tasks. We only use `cargo` task type. 19 | */ 20 | const TASK_TYPE = 'cargo'; 21 | 22 | /** 23 | * Command execution parameters sent by the RLS (as of 1.35). 24 | */ 25 | export interface Execution { 26 | /** 27 | * @deprecated Previously, the usage was not restricted to spawning with a 28 | * file, so the name is changed to reflect the more permissive usage and to be 29 | * less misleading (still used by RLS 1.35). Use `command` instead. 30 | */ 31 | binary?: string; 32 | command?: string; 33 | args: string[]; 34 | env?: { [key: string]: string }; 35 | // NOTE: Not actually sent by RLS but unifies a common execution definition 36 | cwd?: string; 37 | } 38 | 39 | /** 40 | * Creates a Task-used `ShellExecution` from a unified `Execution` interface. 41 | */ 42 | function createShellExecution(execution: Execution): ShellExecution { 43 | const { binary, command, args, cwd, env } = execution; 44 | const cmdLine = `${command || binary} ${args.join(' ')}`; 45 | return new ShellExecution(cmdLine, { cwd, env }); 46 | } 47 | 48 | export function activateTaskProvider(target: WorkspaceFolder): Disposable { 49 | const provider: TaskProvider = { 50 | // Tasks returned by this function are treated as 'auto-detected' [1] and 51 | // are treated a bit differently. They are always available and can be 52 | // only tweaked (and not removed) in tasks.json. 53 | // This is to support npm-style scripts, which store project-specific 54 | // scripts in the project manifest. However, Cargo.toml does not support 55 | // anything like that, so we just try our best to help the user and present 56 | // them with most commonly used `cargo` subcommands (e.g. `build`). 57 | // Since typically they would need to parse their task definitions, an 58 | // optional `autoDetect` configuration is usually provided, which we don't. 59 | // 60 | // [1]: https://code.visualstudio.com/docs/editor/tasks#_task-autodetection 61 | provideTasks: () => detectCargoTasks(target), 62 | // NOTE: Currently unused by VSCode 63 | resolveTask: () => undefined, 64 | }; 65 | 66 | return tasks.registerTaskProvider(TASK_TYPE, provider); 67 | } 68 | 69 | function detectCargoTasks(target: WorkspaceFolder): Task[] { 70 | return [ 71 | { subcommand: 'build', group: TaskGroup.Build }, 72 | { subcommand: 'check', group: TaskGroup.Build }, 73 | { subcommand: 'test', group: TaskGroup.Test }, 74 | { subcommand: 'clean', group: TaskGroup.Clean }, 75 | { subcommand: 'run', group: undefined }, 76 | ] 77 | .map(({ subcommand, group }) => ({ 78 | definition: { subcommand, type: TASK_TYPE }, 79 | label: `cargo ${subcommand} - ${target.name}`, 80 | execution: createShellExecution({ 81 | command: 'cargo', 82 | args: [subcommand], 83 | cwd: target.uri.fsPath, 84 | }), 85 | group, 86 | problemMatchers: ['$rustc'], 87 | })) 88 | .map(task => { 89 | // NOTE: It's important to solely use the VSCode-provided constructor (and 90 | // *not* use object spread operator!) - otherwise the task will not be picked 91 | // up by VSCode. 92 | const vscodeTask = new Task( 93 | task.definition, 94 | target, 95 | task.label, 96 | TASK_SOURCE, 97 | task.execution, 98 | task.problemMatchers, 99 | ); 100 | vscodeTask.group = task.group; 101 | return vscodeTask; 102 | }); 103 | } 104 | 105 | // NOTE: `execution` parameters here are sent by the RLS. 106 | export function runRlsCommand(folder: WorkspaceFolder, execution: Execution) { 107 | const shellExecution = createShellExecution(execution); 108 | const problemMatchers = ['$rustc']; 109 | 110 | return tasks.executeTask( 111 | new Task( 112 | { type: 'shell' }, 113 | folder, 114 | 'External RLS command', 115 | TASK_SOURCE, 116 | shellExecution, 117 | problemMatchers, 118 | ), 119 | ); 120 | } 121 | 122 | /** 123 | * Starts a shell command as a VSCode task, resolves when a task is finished. 124 | * Useful in tandem with setup commands, since the task window is reusable and 125 | * also capable of displaying ANSI terminal colors. Exit codes are not 126 | * supported, however. 127 | */ 128 | export async function runTaskCommand( 129 | { command, args, env, cwd }: Execution, 130 | displayName: string, 131 | folder?: WorkspaceFolder, 132 | ) { 133 | // Task finish callback does not preserve concrete task definitions, we so 134 | // disambiguate finished tasks via executed command line. 135 | const commandLine = `${command} ${args.join(' ')}`; 136 | 137 | const task = new Task( 138 | { type: 'shell' }, 139 | folder || workspace.workspaceFolders![0], 140 | displayName, 141 | TASK_SOURCE, 142 | new ShellExecution(commandLine, { 143 | cwd: cwd || (folder && folder.uri.fsPath), 144 | env, 145 | }), 146 | ); 147 | 148 | return new Promise(resolve => { 149 | const disposable = tasks.onDidEndTask(({ execution }) => { 150 | const taskExecution = execution.task.execution; 151 | if ( 152 | taskExecution instanceof ShellExecution && 153 | taskExecution.commandLine === commandLine 154 | ) { 155 | disposable.dispose(); 156 | resolve(); 157 | } 158 | }); 159 | 160 | tasks.executeTask(task); 161 | }); 162 | } 163 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/run.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as lc from 'vscode-languageclient'; 3 | import * as ra from './lsp_ext'; 4 | import * as tasks from './tasks'; 5 | 6 | import { Ctx } from './ctx'; 7 | import { makeDebugConfig } from './debug'; 8 | import { Config, RunnableEnvCfg } from './config'; 9 | 10 | const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }]; 11 | 12 | export async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true): Promise { 13 | const editor = ctx.activeRustEditor; 14 | const client = ctx.client; 15 | if (!editor || !client) return; 16 | 17 | const textDocument: lc.TextDocumentIdentifier = { 18 | uri: editor.document.uri.toString(), 19 | }; 20 | 21 | const runnables = await client.sendRequest(ra.runnables, { 22 | textDocument, 23 | position: client.code2ProtocolConverter.asPosition( 24 | editor.selection.active, 25 | ), 26 | }); 27 | const items: RunnableQuickPick[] = []; 28 | if (prevRunnable) { 29 | items.push(prevRunnable); 30 | } 31 | for (const r of runnables) { 32 | if ( 33 | prevRunnable && 34 | JSON.stringify(prevRunnable.runnable) === JSON.stringify(r) 35 | ) { 36 | continue; 37 | } 38 | 39 | if (debuggeeOnly && (r.label.startsWith('doctest') || r.label.startsWith('cargo'))) { 40 | continue; 41 | } 42 | items.push(new RunnableQuickPick(r)); 43 | } 44 | 45 | if (items.length === 0) { 46 | // it is the debug case, run always has at least 'cargo check ...' 47 | // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_runnables 48 | vscode.window.showErrorMessage("There's no debug target!"); 49 | return; 50 | } 51 | 52 | return await new Promise((resolve) => { 53 | const disposables: vscode.Disposable[] = []; 54 | const close = (result?: RunnableQuickPick) => { 55 | resolve(result); 56 | disposables.forEach(d => d.dispose()); 57 | }; 58 | 59 | const quickPick = vscode.window.createQuickPick(); 60 | quickPick.items = items; 61 | quickPick.title = "Select Runnable"; 62 | if (showButtons) { 63 | quickPick.buttons = quickPickButtons; 64 | } 65 | disposables.push( 66 | quickPick.onDidHide(() => close()), 67 | quickPick.onDidAccept(() => close(quickPick.selectedItems[0])), 68 | quickPick.onDidTriggerButton((_button) => { 69 | (async () => await makeDebugConfig(ctx, quickPick.activeItems[0].runnable))(); 70 | close(); 71 | }), 72 | quickPick.onDidChangeActive((active) => { 73 | if (showButtons && active.length > 0) { 74 | if (active[0].label.startsWith('cargo')) { 75 | // save button makes no sense for `cargo test` or `cargo check` 76 | quickPick.buttons = []; 77 | } else if (quickPick.buttons.length === 0) { 78 | quickPick.buttons = quickPickButtons; 79 | } 80 | } 81 | }), 82 | quickPick 83 | ); 84 | quickPick.show(); 85 | }); 86 | } 87 | 88 | export class RunnableQuickPick implements vscode.QuickPickItem { 89 | public label: string; 90 | public description?: string | undefined; 91 | public detail?: string | undefined; 92 | public picked?: boolean | undefined; 93 | 94 | constructor(public runnable: ra.Runnable) { 95 | this.label = runnable.label; 96 | } 97 | } 98 | 99 | export function prepareEnv(runnable: ra.Runnable, runnableEnvCfg: RunnableEnvCfg): Record { 100 | const env: Record = { "RUST_BACKTRACE": "short" }; 101 | 102 | if (runnable.args.expectTest) { 103 | env["UPDATE_EXPECT"] = "1"; 104 | } 105 | 106 | Object.assign(env, process.env as { [key: string]: string }); 107 | 108 | if (runnableEnvCfg) { 109 | if (Array.isArray(runnableEnvCfg)) { 110 | for (const it of runnableEnvCfg) { 111 | if (!it.mask || new RegExp(it.mask).test(runnable.label)) { 112 | Object.assign(env, it.env); 113 | } 114 | } 115 | } else { 116 | Object.assign(env, runnableEnvCfg); 117 | } 118 | } 119 | 120 | return env; 121 | } 122 | 123 | export async function createTask(runnable: ra.Runnable, config: Config): Promise { 124 | if (runnable.kind !== "cargo") { 125 | // rust-analyzer supports only one kind, "cargo" 126 | // do not use tasks.TASK_TYPE here, these are completely different meanings. 127 | 128 | throw `Unexpected runnable kind: ${runnable.kind}`; 129 | } 130 | 131 | const args = [...runnable.args.cargoArgs]; // should be a copy! 132 | if (runnable.args.executableArgs.length > 0) { 133 | args.push('--', ...runnable.args.executableArgs); 134 | } 135 | 136 | const definition: tasks.CargoTaskDefinition = { 137 | type: tasks.TASK_TYPE, 138 | command: args[0], // run, test, etc... 139 | args: args.slice(1), 140 | cwd: runnable.args.workspaceRoot || ".", 141 | env: prepareEnv(runnable, config.runnableEnv), 142 | }; 143 | 144 | const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate() 145 | const cargoTask = await tasks.buildCargoTask(target, definition, runnable.label, args, config.cargoRunner, true); 146 | cargoTask.presentationOptions.clear = true; 147 | 148 | return cargoTask; 149 | } 150 | -------------------------------------------------------------------------------- /src/providers/signatureHelpProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { HoverRequest, LanguageClient } from 'vscode-languageclient'; 3 | 4 | export class SignatureHelpProvider implements vscode.SignatureHelpProvider { 5 | private languageClient: LanguageClient; 6 | private previousFunctionPosition?: vscode.Position; 7 | 8 | constructor(lc: LanguageClient) { 9 | this.languageClient = lc; 10 | } 11 | 12 | public provideSignatureHelp( 13 | document: vscode.TextDocument, 14 | position: vscode.Position, 15 | token: vscode.CancellationToken, 16 | context: vscode.SignatureHelpContext, 17 | ): vscode.ProviderResult { 18 | // the current signature help provider uses the hover information from RLS 19 | // and it only has a string representation of the function signature. 20 | // This check makes sure we can easily show the tooltip for multiple parameters, separated by `,` 21 | if (context.triggerCharacter === '(') { 22 | this.previousFunctionPosition = position; 23 | return this.provideHover( 24 | this.languageClient, 25 | document, 26 | position, 27 | token, 28 | ).then(hover => this.hoverToSignatureHelp(hover, position, document)); 29 | } else if (context.triggerCharacter === ',') { 30 | if ( 31 | this.previousFunctionPosition && 32 | position.line === this.previousFunctionPosition.line 33 | ) { 34 | return this.provideHover( 35 | this.languageClient, 36 | document, 37 | this.previousFunctionPosition, 38 | token, 39 | ).then(hover => this.hoverToSignatureHelp(hover, position, document)); 40 | } else { 41 | return null; 42 | } 43 | } else { 44 | if (context.isRetrigger === false) { 45 | this.previousFunctionPosition = undefined; 46 | } 47 | return null; 48 | } 49 | } 50 | 51 | private provideHover( 52 | lc: LanguageClient, 53 | document: vscode.TextDocument, 54 | position: vscode.Position, 55 | token: vscode.CancellationToken, 56 | ): Promise { 57 | return new Promise((resolve, reject) => { 58 | lc.sendRequest( 59 | HoverRequest.type, 60 | lc.code2ProtocolConverter.asTextDocumentPositionParams( 61 | document, 62 | position.translate(0, -1), 63 | ), 64 | token, 65 | ).then( 66 | data => resolve(lc.protocol2CodeConverter.asHover(data)), 67 | error => reject(error), 68 | ); 69 | }); 70 | } 71 | 72 | private hoverToSignatureHelp( 73 | hover: vscode.Hover, 74 | position: vscode.Position, 75 | document: vscode.TextDocument, 76 | ): vscode.SignatureHelp | undefined { 77 | /* 78 | The contents of a hover result has the following structure: 79 | contents:Array[2] 80 | 0:Object 81 | value:" 82 | ```rust 83 | pub fn write(output: &mut dyn Write, args: Arguments) -> Result 84 | ``` 85 | " 86 | 1:Object 87 | value:"The `write` function takes an output stream, and an `Arguments` struct 88 | that can be precompiled with the `format_args!` macro. 89 | The arguments will be formatted according to the specified format string 90 | into the output stream provided. 91 | # Examples 92 | RLS uses the function below to create the tooltip contents shown above: 93 | fn create_tooltip( 94 | the_type: String, 95 | doc_url: Option, 96 | context: Option, 97 | docs: Option, 98 | ) -> Vec {} 99 | This means the first object is the type - function signature, 100 | but for the following, there is no way of certainly knowing which is the 101 | function documentation that we want to display in the tooltip. 102 | 103 | Assuming the context is never populated for a function definition (this might be wrong 104 | and needs further validation, but initial tests show it to hold true in most cases), and 105 | we also assume that most functions contain rather documentation, than just a URL without 106 | any inline documentation, we check the length of contents, and we assume that if there are: 107 | - two objects, they are the signature and docs, and docs is contents[1] 108 | - three objects, they are the signature, URL and docs, and docs is contents[2] 109 | - four objects -- all of them, docs is contents[3] 110 | See https://github.com/rust-lang/rls/blob/master/rls/src/actions/hover.rs#L487-L508. 111 | */ 112 | 113 | // we remove the markdown formatting for the label, as it only accepts strings 114 | const label = (hover.contents[0] as vscode.MarkdownString).value 115 | .replace('```rust', '') 116 | .replace('```', ''); 117 | 118 | // the signature help tooltip is activated on `(` or `,` 119 | // here we make sure the label received is for a function, 120 | // and that we are not showing the hover for the same line 121 | // where we are declaring a function. 122 | if ( 123 | !label.includes('fn') || 124 | document.lineAt(position.line).text.includes('fn ') 125 | ) { 126 | return undefined; 127 | } 128 | 129 | const doc = 130 | hover.contents.length > 1 131 | ? (hover.contents.slice(-1)[0] as vscode.MarkdownString) 132 | : undefined; 133 | const si = new vscode.SignatureInformation(label, doc); 134 | 135 | // without parsing the function definition, we don't have a way to get more info on parameters. 136 | // If RLS supports signature help requests in the future, we can update this. 137 | si.parameters = []; 138 | 139 | const sh = new vscode.SignatureHelp(); 140 | sh.signatures[0] = si; 141 | sh.activeSignature = 0; 142 | 143 | return sh; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/config.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { log } from "./util"; 3 | 4 | export type UpdatesChannel = "stable" | "nightly"; 5 | 6 | export const NIGHTLY_TAG = "nightly"; 7 | 8 | export type RunnableEnvCfg = undefined | Record | { mask?: string; env: Record }[]; 9 | 10 | export class Config { 11 | readonly extensionId = "matklad.rust-analyzer"; 12 | 13 | readonly rootSection = "rust-analyzer"; 14 | private readonly requiresReloadOpts = [ 15 | "serverPath", 16 | "cargo", 17 | "procMacro", 18 | "files", 19 | "highlighting", 20 | "updates.channel", 21 | "lens", // works as lens.* 22 | "hoverActions", // works as hoverActions.* 23 | ] 24 | .map(opt => `${this.rootSection}.${opt}`); 25 | 26 | readonly package: { 27 | version: string; 28 | releaseTag: string | null; 29 | enableProposedApi: boolean | undefined; 30 | } = vscode.extensions.getExtension(this.extensionId)!.packageJSON; 31 | 32 | readonly globalStoragePath: string; 33 | 34 | constructor(ctx: vscode.ExtensionContext) { 35 | this.globalStoragePath = ctx.globalStoragePath; 36 | vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, ctx.subscriptions); 37 | this.refreshLogging(); 38 | } 39 | 40 | private refreshLogging() { 41 | log.setEnabled(this.traceExtension); 42 | log.info("Extension version:", this.package.version); 43 | 44 | const cfg = Object.entries(this.cfg).filter(([_, val]) => !(val instanceof Function)); 45 | log.info("Using configuration", Object.fromEntries(cfg)); 46 | } 47 | 48 | private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) { 49 | this.refreshLogging(); 50 | 51 | const requiresReloadOpt = this.requiresReloadOpts.find( 52 | opt => event.affectsConfiguration(opt) 53 | ); 54 | 55 | if (!requiresReloadOpt) return; 56 | 57 | const userResponse = await vscode.window.showInformationMessage( 58 | `Changing "${requiresReloadOpt}" requires a reload`, 59 | "Reload now" 60 | ); 61 | 62 | if (userResponse === "Reload now") { 63 | await vscode.commands.executeCommand("workbench.action.reloadWindow"); 64 | } 65 | } 66 | 67 | // We don't do runtime config validation here for simplicity. More on stackoverflow: 68 | // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension 69 | 70 | private get cfg(): vscode.WorkspaceConfiguration { 71 | return vscode.workspace.getConfiguration(this.rootSection); 72 | } 73 | 74 | /** 75 | * Beware that postfix `!` operator erases both `null` and `undefined`. 76 | * This is why the following doesn't work as expected: 77 | * 78 | * ```ts 79 | * const nullableNum = vscode 80 | * .workspace 81 | * .getConfiguration 82 | * .getConfiguration("rust-analyer") 83 | * .get(path)!; 84 | * 85 | * // What happens is that type of `nullableNum` is `number` but not `null | number`: 86 | * const fullFledgedNum: number = nullableNum; 87 | * ``` 88 | * So this getter handles this quirk by not requiring the caller to use postfix `!` 89 | */ 90 | private get(path: string): T { 91 | return this.cfg.get(path)!; 92 | } 93 | 94 | get serverPath() { return this.get("serverPath"); } 95 | get channel() { return this.get("updates.channel"); } 96 | get askBeforeDownload() { return this.get("updates.askBeforeDownload"); } 97 | get traceExtension() { return this.get("trace.extension"); } 98 | 99 | get inlayHints() { 100 | return { 101 | enable: this.get("inlayHints.enable"), 102 | typeHints: this.get("inlayHints.typeHints"), 103 | parameterHints: this.get("inlayHints.parameterHints"), 104 | chainingHints: this.get("inlayHints.chainingHints"), 105 | maxLength: this.get("inlayHints.maxLength"), 106 | }; 107 | } 108 | 109 | get checkOnSave() { 110 | return { 111 | command: this.get("checkOnSave.command"), 112 | }; 113 | } 114 | 115 | get cargoRunner() { 116 | return this.get("cargoRunner"); 117 | } 118 | 119 | get runnableEnv() { 120 | return this.get("runnableEnv"); 121 | } 122 | 123 | get debug() { 124 | // "/rustc/" used by suggestions only. 125 | const { ["/rustc/"]: _, ...sourceFileMap } = this.get>("debug.sourceFileMap"); 126 | 127 | return { 128 | engine: this.get("debug.engine"), 129 | engineSettings: this.get("debug.engineSettings"), 130 | openDebugPane: this.get("debug.openDebugPane"), 131 | sourceFileMap: sourceFileMap 132 | }; 133 | } 134 | 135 | get lens() { 136 | return { 137 | enable: this.get("lens.enable"), 138 | run: this.get("lens.run"), 139 | debug: this.get("lens.debug"), 140 | implementations: this.get("lens.implementations"), 141 | }; 142 | } 143 | 144 | get hoverActions() { 145 | return { 146 | enable: this.get("hoverActions.enable"), 147 | implementations: this.get("hoverActions.implementations"), 148 | run: this.get("hoverActions.run"), 149 | debug: this.get("hoverActions.debug"), 150 | gotoTypeDef: this.get("hoverActions.gotoTypeDef"), 151 | }; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/debug.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | import * as ra from './lsp_ext'; 5 | 6 | import { Cargo } from './toolchain'; 7 | import { Ctx } from "./ctx"; 8 | import { prepareEnv } from "./run"; 9 | 10 | const debugOutput = vscode.window.createOutputChannel("Debug"); 11 | type DebugConfigProvider = (config: ra.Runnable, executable: string, env: Record, sourceFileMap?: Record) => vscode.DebugConfiguration; 12 | 13 | export async function makeDebugConfig(ctx: Ctx, runnable: ra.Runnable): Promise { 14 | const scope = ctx.activeRustEditor?.document.uri; 15 | if (!scope) return; 16 | 17 | const debugConfig = await getDebugConfiguration(ctx, runnable); 18 | if (!debugConfig) return; 19 | 20 | const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope); 21 | const configurations = wsLaunchSection.get("configurations") || []; 22 | 23 | const index = configurations.findIndex(c => c.name === debugConfig.name); 24 | if (index !== -1) { 25 | const answer = await vscode.window.showErrorMessage(`Launch configuration '${debugConfig.name}' already exists!`, 'Cancel', 'Update'); 26 | if (answer === "Cancel") return; 27 | 28 | configurations[index] = debugConfig; 29 | } else { 30 | configurations.push(debugConfig); 31 | } 32 | 33 | await wsLaunchSection.update("configurations", configurations); 34 | } 35 | 36 | export async function startDebugSession(ctx: Ctx, runnable: ra.Runnable): Promise { 37 | let debugConfig: vscode.DebugConfiguration | undefined = undefined; 38 | let message = ""; 39 | 40 | const wsLaunchSection = vscode.workspace.getConfiguration("launch"); 41 | const configurations = wsLaunchSection.get("configurations") || []; 42 | 43 | const index = configurations.findIndex(c => c.name === runnable.label); 44 | if (-1 !== index) { 45 | debugConfig = configurations[index]; 46 | message = " (from launch.json)"; 47 | debugOutput.clear(); 48 | } else { 49 | debugConfig = await getDebugConfiguration(ctx, runnable); 50 | } 51 | 52 | if (!debugConfig) return false; 53 | 54 | debugOutput.appendLine(`Launching debug configuration${message}:`); 55 | debugOutput.appendLine(JSON.stringify(debugConfig, null, 2)); 56 | return vscode.debug.startDebugging(undefined, debugConfig); 57 | } 58 | 59 | async function getDebugConfiguration(ctx: Ctx, runnable: ra.Runnable): Promise { 60 | const editor = ctx.activeRustEditor; 61 | if (!editor) return; 62 | 63 | const knownEngines: Record = { 64 | "vadimcn.vscode-lldb": getLldbDebugConfig, 65 | "ms-vscode.cpptools": getCppvsDebugConfig 66 | }; 67 | const debugOptions = ctx.config.debug; 68 | 69 | let debugEngine = null; 70 | if (debugOptions.engine === "auto") { 71 | for (var engineId in knownEngines) { 72 | debugEngine = vscode.extensions.getExtension(engineId); 73 | if (debugEngine) break; 74 | } 75 | } else { 76 | debugEngine = vscode.extensions.getExtension(debugOptions.engine); 77 | } 78 | 79 | if (!debugEngine) { 80 | vscode.window.showErrorMessage(`Install [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb)` 81 | + ` or [MS C++ tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) extension for debugging.`); 82 | return; 83 | } 84 | 85 | debugOutput.clear(); 86 | if (ctx.config.debug.openDebugPane) { 87 | debugOutput.show(true); 88 | } 89 | 90 | const wsFolder = path.normalize(vscode.workspace.workspaceFolders![0].uri.fsPath); // folder exists or RA is not active. 91 | function simplifyPath(p: string): string { 92 | return path.normalize(p).replace(wsFolder, '${workspaceRoot}'); 93 | } 94 | 95 | const executable = await getDebugExecutable(runnable); 96 | const env = prepareEnv(runnable, ctx.config.runnableEnv); 97 | const debugConfig = knownEngines[debugEngine.id](runnable, simplifyPath(executable), env, debugOptions.sourceFileMap); 98 | if (debugConfig.type in debugOptions.engineSettings) { 99 | const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type]; 100 | for (var key in settingsMap) { 101 | debugConfig[key] = settingsMap[key]; 102 | } 103 | } 104 | 105 | if (debugConfig.name === "run binary") { 106 | // The LSP side: crates\rust-analyzer\src\main_loop\handlers.rs, 107 | // fn to_lsp_runnable(...) with RunnableKind::Bin 108 | debugConfig.name = `run ${path.basename(executable)}`; 109 | } 110 | 111 | if (debugConfig.cwd) { 112 | debugConfig.cwd = simplifyPath(debugConfig.cwd); 113 | } 114 | 115 | return debugConfig; 116 | } 117 | 118 | async function getDebugExecutable(runnable: ra.Runnable): Promise { 119 | const cargo = new Cargo(runnable.args.workspaceRoot || '.', debugOutput); 120 | const executable = await cargo.executableFromArgs(runnable.args.cargoArgs); 121 | 122 | // if we are here, there were no compilation errors. 123 | return executable; 124 | } 125 | 126 | function getLldbDebugConfig(runnable: ra.Runnable, executable: string, env: Record, sourceFileMap?: Record): vscode.DebugConfiguration { 127 | return { 128 | type: "lldb", 129 | request: "launch", 130 | name: runnable.label, 131 | program: executable, 132 | args: runnable.args.executableArgs, 133 | cwd: runnable.args.workspaceRoot, 134 | sourceMap: sourceFileMap, 135 | sourceLanguages: ["rust"], 136 | env 137 | }; 138 | } 139 | 140 | function getCppvsDebugConfig(runnable: ra.Runnable, executable: string, env: Record, sourceFileMap?: Record): vscode.DebugConfiguration { 141 | return { 142 | type: (os.platform() === "win32") ? "cppvsdbg" : "cppdbg", 143 | request: "launch", 144 | name: runnable.label, 145 | program: executable, 146 | args: runnable.args.executableArgs, 147 | cwd: runnable.args.workspaceRoot, 148 | sourceFileMap, 149 | env, 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/toolchain.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import * as os from 'os'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import * as readline from 'readline'; 6 | import { OutputChannel } from 'vscode'; 7 | import { log, memoize } from './util'; 8 | 9 | interface CompilationArtifact { 10 | fileName: string; 11 | name: string; 12 | kind: string; 13 | isTest: boolean; 14 | } 15 | 16 | export interface ArtifactSpec { 17 | cargoArgs: string[]; 18 | filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[]; 19 | } 20 | 21 | export class Cargo { 22 | constructor(readonly rootFolder: string, readonly output: OutputChannel) { } 23 | 24 | // Made public for testing purposes 25 | static artifactSpec(args: readonly string[]): ArtifactSpec { 26 | const cargoArgs = [...args, "--message-format=json"]; 27 | 28 | // arguments for a runnable from the quick pick should be updated. 29 | // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens 30 | switch (cargoArgs[0]) { 31 | case "run": cargoArgs[0] = "build"; break; 32 | case "test": { 33 | if (!cargoArgs.includes("--no-run")) { 34 | cargoArgs.push("--no-run"); 35 | } 36 | break; 37 | } 38 | } 39 | 40 | const result: ArtifactSpec = { cargoArgs: cargoArgs }; 41 | if (cargoArgs[0] === "test") { 42 | // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests 43 | // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"} 44 | result.filter = (artifacts) => artifacts.filter(it => it.isTest); 45 | } 46 | 47 | return result; 48 | } 49 | 50 | private async getArtifacts(spec: ArtifactSpec): Promise { 51 | const artifacts: CompilationArtifact[] = []; 52 | 53 | try { 54 | await this.runCargo(spec.cargoArgs, 55 | message => { 56 | if (message.reason === 'compiler-artifact' && message.executable) { 57 | const isBinary = message.target.crate_types.includes('bin'); 58 | const isBuildScript = message.target.kind.includes('custom-build'); 59 | if ((isBinary && !isBuildScript) || message.profile.test) { 60 | artifacts.push({ 61 | fileName: message.executable, 62 | name: message.target.name, 63 | kind: message.target.kind[0], 64 | isTest: message.profile.test 65 | }); 66 | } 67 | } else if (message.reason === 'compiler-message') { 68 | this.output.append(message.message.rendered); 69 | } 70 | }, 71 | stderr => this.output.append(stderr), 72 | ); 73 | } catch (err) { 74 | this.output.show(true); 75 | throw new Error(`Cargo invocation has failed: ${err}`); 76 | } 77 | 78 | return spec.filter?.(artifacts) ?? artifacts; 79 | } 80 | 81 | async executableFromArgs(args: readonly string[]): Promise { 82 | const artifacts = await this.getArtifacts(Cargo.artifactSpec(args)); 83 | 84 | if (artifacts.length === 0) { 85 | throw new Error('No compilation artifacts'); 86 | } else if (artifacts.length > 1) { 87 | throw new Error('Multiple compilation artifacts are not supported.'); 88 | } 89 | 90 | return artifacts[0].fileName; 91 | } 92 | 93 | private runCargo( 94 | cargoArgs: string[], 95 | onStdoutJson: (obj: any) => void, 96 | onStderrString: (data: string) => void 97 | ): Promise { 98 | return new Promise((resolve, reject) => { 99 | const cargo = cp.spawn(cargoPath(), cargoArgs, { 100 | stdio: ['ignore', 'pipe', 'pipe'], 101 | cwd: this.rootFolder 102 | }); 103 | 104 | cargo.on('error', err => reject(new Error(`could not launch cargo: ${err}`))); 105 | 106 | cargo.stderr.on('data', chunk => onStderrString(chunk.toString())); 107 | 108 | const rl = readline.createInterface({ input: cargo.stdout }); 109 | rl.on('line', line => { 110 | const message = JSON.parse(line); 111 | onStdoutJson(message); 112 | }); 113 | 114 | cargo.on('exit', (exitCode, _) => { 115 | if (exitCode === 0) 116 | resolve(exitCode); 117 | else 118 | reject(new Error(`exit code: ${exitCode}.`)); 119 | }); 120 | }); 121 | } 122 | } 123 | 124 | /** Mirrors `ra_toolchain::cargo()` implementation */ 125 | export function cargoPath(): string { 126 | return getPathForExecutable("cargo"); 127 | } 128 | 129 | /** Mirrors `ra_toolchain::get_path_for_executable()` implementation */ 130 | export const getPathForExecutable = memoize( 131 | // We apply caching to decrease file-system interactions 132 | (executableName: "cargo" | "rustc" | "rustup"): string => { 133 | { 134 | const envVar = process.env[executableName.toUpperCase()]; 135 | if (envVar) return envVar; 136 | } 137 | 138 | if (lookupInPath(executableName)) return executableName; 139 | 140 | try { 141 | // hmm, `os.homedir()` seems to be infallible 142 | // it is not mentioned in docs and cannot be infered by the type signature... 143 | const standardPath = path.join(os.homedir(), ".cargo", "bin", executableName); 144 | 145 | if (isFile(standardPath)) return standardPath; 146 | } catch (err) { 147 | log.error("Failed to read the fs info", err); 148 | } 149 | return executableName; 150 | } 151 | ); 152 | 153 | function lookupInPath(exec: string): boolean { 154 | const paths = process.env.PATH ?? "";; 155 | 156 | const candidates = paths.split(path.delimiter).flatMap(dirInPath => { 157 | const candidate = path.join(dirInPath, exec); 158 | return os.type() === "Windows_NT" 159 | ? [candidate, `${candidate}.exe`] 160 | : [candidate]; 161 | }); 162 | 163 | return candidates.some(isFile); 164 | } 165 | 166 | function isFile(suspectPath: string): boolean { 167 | // It is not mentionned in docs, but `statSync()` throws an error when 168 | // the path doesn't exist 169 | try { 170 | return fs.statSync(suspectPath).isFile(); 171 | } catch { 172 | return false; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust support for Visual Studio Code (deprecated) 2 | 3 | [![](https://vsmarketplacebadge.apphb.com/version/rust-lang.rust.svg)](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust) 4 | [![VSCode + Node.js CI](https://img.shields.io/github/workflow/status/rust-lang/rls-vscode/VSCode%20+%20Node.js%20CI.svg?logo=github)](https://github.com/rust-lang/rls-vscode/actions?query=workflow%3A%22VSCode+%2B+Node.js+CI%22) 5 | 6 | ---- 7 | 8 | > **Warning** 9 | > # This extension is no longer maintained. 10 | > This has been replaced by the [**rust-analyzer extension**](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer). 11 | 12 | ----- 13 | 14 | Adds language support for Rust to Visual Studio Code. Supports: 15 | 16 | * code completion 17 | * jump to definition, peek definition, find all references, symbol search 18 | * types and documentation on hover 19 | * code formatting 20 | * refactoring (rename, deglob) 21 | * error squiggles and apply suggestions from errors 22 | * snippets 23 | * build tasks 24 | 25 | Rust support is powered by a separate [language server](https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/) - 26 | either by the official [Rust Language Server](https://github.com/rust-lang/rls) (RLS) or 27 | [rust-analyzer](https://github.com/rust-analyzer/rust-analyzer), depending on the user's 28 | preference. If you don't have it installed, the extension will install it for 29 | you (with permission). 30 | 31 | This extension is built and maintained by the Rust 32 | [IDEs and editors team](https://www.rust-lang.org/en-US/team.html#Dev-tools-team). 33 | Our focus is on providing 34 | a stable, high quality extension that makes the best use of the respective language 35 | server. We aim to support as many features as possible, but our priority is 36 | supporting the essential features as well as possible. 37 | 38 | For support, please file an 39 | [issue on the repo](https://github.com/rust-lang/rls-vscode/issues/new) 40 | or talk to us [on Discord](https://discordapp.com/invite/rust-lang). 41 | For RLS, there is also some [troubleshooting and debugging](https://github.com/rust-lang/rls/blob/master/debugging.md) advice. 42 | 43 | ## Contribution 44 | 45 | Contributing code, tests, documentation, and bug reports is appreciated! For 46 | more details see [contributing.md](contributing.md). 47 | 48 | 49 | ## Quick start 50 | 51 | 1. Install [rustup](https://www.rustup.rs/) (Rust toolchain manager). 52 | 2. Install this extension from [the VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust) 53 | (or by entering `ext install rust-lang.rust` at the command palette Ctrl+P). 54 | 3. (Skip this step if you already have Rust projects that you'd like to work on.) 55 | Create a new Rust project by following [these instructions](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html). 56 | 4. Open a Rust project (`File > Add Folder to Workspace...`). Open the folder for the whole 57 | project (i.e., the folder containing `Cargo.toml`, not the `src` folder). 58 | 5. You'll be prompted to install the Rust server. Once installed, it should start 59 | analyzing your project (RLS will also have to to build the project). 60 | 61 | 62 | ## Configuration 63 | 64 | This extension provides options in VSCode's configuration settings. These 65 | include `rust.*`, which are passed directly to RLS, and the `rust-client.*` 66 | , which mostly deal with how to spawn it or debug it. 67 | You can find the settings under `File > Preferences > Settings`; they all 68 | have IntelliSense help. 69 | 70 | Examples: 71 | 72 | * `rust.show_warnings` - set to false to silence warnings in the editor. 73 | * `rust.all_targets` - build and index code for all targets (i.e., integration tests, examples, and benches) 74 | * `rust.cfg_test` - build and index test code (i.e., code with `#[cfg(test)]`/`#[test]`) 75 | * `rust-client.channel` - specifies from which toolchain the RLS should be spawned 76 | 77 | > **_TIP:_** To select the underlying language server, set `rust-client.engine` accordingly! 78 | 79 | ## Features 80 | 81 | ### Snippets 82 | 83 | Snippets are code templates which expand into common boilerplate. IntelliSense 84 | includes snippet names as options when you type; select one by pressing 85 | enter. You can move to the next snippet 'hole' in the template by 86 | pressing tab. We provide the following snippets: 87 | 88 | * `for` - a for loop 89 | * `macro_rules` - declare a macro 90 | * `if let` - an `if let` statement for executing code only when a pattern matches 91 | * `spawn` - spawn a thread 92 | * `extern crate` - insert an `extern crate` statement 93 | 94 | This extension is deliberately conservative about snippets and doesn't include 95 | too many. If you want more, check out 96 | [Trusty Rusty Snippets](https://marketplace.visualstudio.com/items?itemName=polypus74.trusty-rusty-snippets). 97 | 98 | ### Tasks 99 | 100 | The plugin provides tasks for building, running, and testing using the relevant 101 | cargo commands. You can build using ctrl+shift+b(Win/Linux), cmd+shift+b(macOS). 102 | Access other tasks via `Run Task` in the command palette. 103 | 104 | The plugin writes these into `tasks.json`. The plugin will not overwrite 105 | existing tasks, so you can customise these tasks. To refresh back to the 106 | defaults, delete `tasks.json` and restart VSCode. 107 | 108 | 109 | ## Format on save 110 | 111 | To enable formatting on save, you need to set the `editor.formatOnSave` setting 112 | to `true`. Find it under `File > Preferences > Settings`. 113 | 114 | 115 | ## Requirements 116 | 117 | * [Rustup](https://www.rustup.rs/), 118 | * A Rust toolchain (the extension will configure this for you, with permission), 119 | * `rls`, `rust-src`, and `rust-analysis` components (the extension will install 120 | these for you, with permission). Only `rust-src` is required when using 121 | rust-analyzer. 122 | 123 | 124 | ## Implementation 125 | 126 | Both language servers can use Cargo to get more information about Rust projects 127 | and both use [`rustfmt`](https://github.com/rust-lang/rustfmt/) extensively to 128 | format the code. 129 | 130 | [RLS](https://github.com/rust-lang/rls) uses Cargo and also the Rust compiler 131 | ([`rustc`](https://github.com/rust-lang/rust/)) in a more direct fashion, where 132 | it builds the project and reuses the data computed by the compiler itself. To 133 | provide code completion it uses a separate tool called 134 | [`racer`](https://github.com/racer-rust/racer). 135 | 136 | [Rust Analyzer](https://github.com/rust-analyzer/rust-analyzer) is a separate 137 | compiler frontend for the Rust language that doesn't use the Rust compiler 138 | ([`rustc`](https://github.com/rust-lang/rust/)) directly but rather performs its 139 | own analysis that's tailor-fitted to the editor/IDE use case. 140 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/ast_inspector.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Ctx, Disposable } from './ctx'; 4 | import { RustEditor, isRustEditor } from './util'; 5 | 6 | // FIXME: consider implementing this via the Tree View API? 7 | // https://code.visualstudio.com/api/extension-guides/tree-view 8 | export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable { 9 | private readonly astDecorationType = vscode.window.createTextEditorDecorationType({ 10 | borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'), 11 | borderStyle: "solid", 12 | borderWidth: "2px", 13 | }); 14 | private rustEditor: undefined | RustEditor; 15 | 16 | // Lazy rust token range -> syntax tree file range. 17 | private readonly rust2Ast = new Lazy(() => { 18 | const astEditor = this.findAstTextEditor(); 19 | if (!this.rustEditor || !astEditor) return undefined; 20 | 21 | const buf: [vscode.Range, vscode.Range][] = []; 22 | for (let i = 0; i < astEditor.document.lineCount; ++i) { 23 | const astLine = astEditor.document.lineAt(i); 24 | 25 | // Heuristically look for nodes with quoted text (which are token nodes) 26 | const isTokenNode = astLine.text.lastIndexOf('"') >= 0; 27 | if (!isTokenNode) continue; 28 | 29 | const rustRange = this.parseRustTextRange(this.rustEditor.document, astLine.text); 30 | if (!rustRange) continue; 31 | 32 | buf.push([rustRange, this.findAstNodeRange(astLine)]); 33 | } 34 | return buf; 35 | }); 36 | 37 | constructor(ctx: Ctx) { 38 | ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: 'rust-analyzer' }, this)); 39 | ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this)); 40 | vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions); 41 | vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions); 42 | vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions); 43 | 44 | ctx.pushCleanup(this); 45 | } 46 | dispose() { 47 | this.setRustEditor(undefined); 48 | } 49 | 50 | private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { 51 | if (this.rustEditor && event.document.uri.toString() === this.rustEditor.document.uri.toString()) { 52 | this.rust2Ast.reset(); 53 | } 54 | } 55 | 56 | private onDidCloseTextDocument(doc: vscode.TextDocument) { 57 | if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) { 58 | this.setRustEditor(undefined); 59 | } 60 | } 61 | 62 | private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) { 63 | if (!this.findAstTextEditor()) { 64 | this.setRustEditor(undefined); 65 | return; 66 | } 67 | this.setRustEditor(editors.find(isRustEditor)); 68 | } 69 | 70 | private findAstTextEditor(): undefined | vscode.TextEditor { 71 | return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === 'rust-analyzer'); 72 | } 73 | 74 | private setRustEditor(newRustEditor: undefined | RustEditor) { 75 | if (this.rustEditor && this.rustEditor !== newRustEditor) { 76 | this.rustEditor.setDecorations(this.astDecorationType, []); 77 | this.rust2Ast.reset(); 78 | } 79 | this.rustEditor = newRustEditor; 80 | } 81 | 82 | // additional positional params are omitted 83 | provideDefinition(doc: vscode.TextDocument, pos: vscode.Position): vscode.ProviderResult { 84 | if (!this.rustEditor || doc.uri.toString() !== this.rustEditor.document.uri.toString()) return; 85 | 86 | const astEditor = this.findAstTextEditor(); 87 | if (!astEditor) return; 88 | 89 | const rust2AstRanges = this.rust2Ast.get()?.find(([rustRange, _]) => rustRange.contains(pos)); 90 | if (!rust2AstRanges) return; 91 | 92 | const [rustFileRange, astFileRange] = rust2AstRanges; 93 | 94 | astEditor.revealRange(astFileRange); 95 | astEditor.selection = new vscode.Selection(astFileRange.start, astFileRange.end); 96 | 97 | return [{ 98 | targetRange: astFileRange, 99 | targetUri: astEditor.document.uri, 100 | originSelectionRange: rustFileRange, 101 | targetSelectionRange: astFileRange, 102 | }]; 103 | } 104 | 105 | // additional positional params are omitted 106 | provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult { 107 | if (!this.rustEditor) return; 108 | 109 | const astFileLine = doc.lineAt(hoverPosition.line); 110 | 111 | const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text); 112 | if (!rustFileRange) return; 113 | 114 | this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]); 115 | this.rustEditor.revealRange(rustFileRange); 116 | 117 | const rustSourceCode = this.rustEditor.document.getText(rustFileRange); 118 | const astFileRange = this.findAstNodeRange(astFileLine); 119 | 120 | return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange); 121 | } 122 | 123 | private findAstNodeRange(astLine: vscode.TextLine): vscode.Range { 124 | const lineOffset = astLine.range.start; 125 | const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex); 126 | const end = lineOffset.translate(undefined, astLine.text.trimEnd().length); 127 | return new vscode.Range(begin, end); 128 | } 129 | 130 | private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range { 131 | const parsedRange = /(\d+)\.\.(\d+)/.exec(astLine); 132 | if (!parsedRange) return; 133 | 134 | const [begin, end] = parsedRange 135 | .slice(1) 136 | .map(off => this.positionAt(doc, +off)); 137 | 138 | return new vscode.Range(begin, end); 139 | } 140 | 141 | // Memoize the last value, otherwise the CPU is at 100% single core 142 | // with quadratic lookups when we build rust2Ast cache 143 | cache?: { doc: vscode.TextDocument; offset: number; line: number }; 144 | 145 | positionAt(doc: vscode.TextDocument, targetOffset: number): vscode.Position { 146 | if (doc.eol === vscode.EndOfLine.LF) { 147 | return doc.positionAt(targetOffset); 148 | } 149 | 150 | // Dirty workaround for crlf line endings 151 | // We are still in this prehistoric era of carriage returns here... 152 | 153 | let line = 0; 154 | let offset = 0; 155 | 156 | const cache = this.cache; 157 | if (cache?.doc === doc && cache.offset <= targetOffset) { 158 | ({ line, offset } = cache); 159 | } 160 | 161 | while (true) { 162 | const lineLenWithLf = doc.lineAt(line).text.length + 1; 163 | if (offset + lineLenWithLf > targetOffset) { 164 | this.cache = { doc, offset, line }; 165 | return doc.positionAt(targetOffset + line); 166 | } 167 | offset += lineLenWithLf; 168 | line += 1; 169 | } 170 | } 171 | } 172 | 173 | class Lazy { 174 | val: undefined | T; 175 | 176 | constructor(private readonly compute: () => undefined | T) { } 177 | 178 | get() { 179 | return this.val ?? (this.val = this.compute()); 180 | } 181 | 182 | reset() { 183 | this.val = undefined; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/inlay_hints.ts: -------------------------------------------------------------------------------- 1 | import * as lc from "vscode-languageclient"; 2 | import * as vscode from 'vscode'; 3 | import * as ra from './lsp_ext'; 4 | 5 | import { Ctx, Disposable } from './ctx'; 6 | import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, sleep } from './util'; 7 | 8 | 9 | export function activateInlayHints(ctx: Ctx) { 10 | const maybeUpdater = { 11 | updater: null as null | HintsUpdater, 12 | async onConfigChange() { 13 | const anyEnabled = ctx.config.inlayHints.typeHints 14 | || ctx.config.inlayHints.parameterHints 15 | || ctx.config.inlayHints.chainingHints; 16 | const enabled = ctx.config.inlayHints.enable && anyEnabled; 17 | 18 | if (!enabled) return this.dispose(); 19 | 20 | await sleep(100); 21 | if (this.updater) { 22 | this.updater.syncCacheAndRenderHints(); 23 | } else { 24 | this.updater = new HintsUpdater(ctx); 25 | } 26 | }, 27 | dispose() { 28 | this.updater?.dispose(); 29 | this.updater = null; 30 | } 31 | }; 32 | 33 | ctx.pushCleanup(maybeUpdater); 34 | 35 | vscode.workspace.onDidChangeConfiguration( 36 | maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions 37 | ); 38 | 39 | maybeUpdater.onConfigChange(); 40 | } 41 | 42 | 43 | const typeHints = { 44 | decorationType: vscode.window.createTextEditorDecorationType({ 45 | after: { 46 | color: new vscode.ThemeColor('rust_analyzer.inlayHint'), 47 | fontStyle: "normal", 48 | } 49 | }), 50 | 51 | toDecoration(hint: ra.InlayHint.TypeHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions { 52 | return { 53 | range: conv.asRange(hint.range), 54 | renderOptions: { after: { contentText: `: ${hint.label}` } } 55 | }; 56 | } 57 | }; 58 | 59 | const paramHints = { 60 | decorationType: vscode.window.createTextEditorDecorationType({ 61 | before: { 62 | color: new vscode.ThemeColor('rust_analyzer.inlayHint'), 63 | fontStyle: "normal", 64 | } 65 | }), 66 | 67 | toDecoration(hint: ra.InlayHint.ParamHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions { 68 | return { 69 | range: conv.asRange(hint.range), 70 | renderOptions: { before: { contentText: `${hint.label}: ` } } 71 | }; 72 | } 73 | }; 74 | 75 | const chainingHints = { 76 | decorationType: vscode.window.createTextEditorDecorationType({ 77 | after: { 78 | color: new vscode.ThemeColor('rust_analyzer.inlayHint'), 79 | fontStyle: "normal", 80 | } 81 | }), 82 | 83 | toDecoration(hint: ra.InlayHint.ChainingHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions { 84 | return { 85 | range: conv.asRange(hint.range), 86 | renderOptions: { after: { contentText: ` ${hint.label}` } } 87 | }; 88 | } 89 | }; 90 | 91 | class HintsUpdater implements Disposable { 92 | private sourceFiles = new Map(); // map Uri -> RustSourceFile 93 | private readonly disposables: Disposable[] = []; 94 | 95 | constructor(private readonly ctx: Ctx) { 96 | vscode.window.onDidChangeVisibleTextEditors( 97 | this.onDidChangeVisibleTextEditors, 98 | this, 99 | this.disposables 100 | ); 101 | 102 | vscode.workspace.onDidChangeTextDocument( 103 | this.onDidChangeTextDocument, 104 | this, 105 | this.disposables 106 | ); 107 | 108 | // Set up initial cache shape 109 | ctx.visibleRustEditors.forEach(editor => this.sourceFiles.set( 110 | editor.document.uri.toString(), 111 | { 112 | document: editor.document, 113 | inlaysRequest: null, 114 | cachedDecorations: null 115 | } 116 | )); 117 | 118 | this.syncCacheAndRenderHints(); 119 | } 120 | 121 | dispose() { 122 | this.sourceFiles.forEach(file => file.inlaysRequest?.cancel()); 123 | this.ctx.visibleRustEditors.forEach(editor => this.renderDecorations(editor, { param: [], type: [], chaining: [] })); 124 | this.disposables.forEach(d => d.dispose()); 125 | } 126 | 127 | onDidChangeTextDocument({ contentChanges, document }: vscode.TextDocumentChangeEvent) { 128 | if (contentChanges.length === 0 || !isRustDocument(document)) return; 129 | this.syncCacheAndRenderHints(); 130 | } 131 | 132 | syncCacheAndRenderHints() { 133 | // FIXME: make inlayHints request pass an array of files? 134 | this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => { 135 | if (!hints) return; 136 | 137 | file.cachedDecorations = this.hintsToDecorations(hints); 138 | 139 | for (const editor of this.ctx.visibleRustEditors) { 140 | if (editor.document.uri.toString() === uri) { 141 | this.renderDecorations(editor, file.cachedDecorations); 142 | } 143 | } 144 | })); 145 | } 146 | 147 | onDidChangeVisibleTextEditors() { 148 | const newSourceFiles = new Map(); 149 | 150 | // Rerendering all, even up-to-date editors for simplicity 151 | this.ctx.visibleRustEditors.forEach(async editor => { 152 | const uri = editor.document.uri.toString(); 153 | const file = this.sourceFiles.get(uri) ?? { 154 | document: editor.document, 155 | inlaysRequest: null, 156 | cachedDecorations: null 157 | }; 158 | newSourceFiles.set(uri, file); 159 | 160 | // No text documents changed, so we may try to use the cache 161 | if (!file.cachedDecorations) { 162 | const hints = await this.fetchHints(file); 163 | if (!hints) return; 164 | 165 | file.cachedDecorations = this.hintsToDecorations(hints); 166 | } 167 | 168 | this.renderDecorations(editor, file.cachedDecorations); 169 | }); 170 | 171 | // Cancel requests for no longer visible (disposed) source files 172 | this.sourceFiles.forEach((file, uri) => { 173 | if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel(); 174 | }); 175 | 176 | this.sourceFiles = newSourceFiles; 177 | } 178 | 179 | private renderDecorations(editor: RustEditor, decorations: InlaysDecorations) { 180 | editor.setDecorations(typeHints.decorationType, decorations.type); 181 | editor.setDecorations(paramHints.decorationType, decorations.param); 182 | editor.setDecorations(chainingHints.decorationType, decorations.chaining); 183 | } 184 | 185 | private hintsToDecorations(hints: ra.InlayHint[]): InlaysDecorations { 186 | const decorations: InlaysDecorations = { type: [], param: [], chaining: [] }; 187 | const conv = this.ctx.client.protocol2CodeConverter; 188 | 189 | for (const hint of hints) { 190 | switch (hint.kind) { 191 | case ra.InlayHint.Kind.TypeHint: { 192 | decorations.type.push(typeHints.toDecoration(hint, conv)); 193 | continue; 194 | } 195 | case ra.InlayHint.Kind.ParamHint: { 196 | decorations.param.push(paramHints.toDecoration(hint, conv)); 197 | continue; 198 | } 199 | case ra.InlayHint.Kind.ChainingHint: { 200 | decorations.chaining.push(chainingHints.toDecoration(hint, conv)); 201 | continue; 202 | } 203 | } 204 | } 205 | return decorations; 206 | } 207 | 208 | private async fetchHints(file: RustSourceFile): Promise { 209 | file.inlaysRequest?.cancel(); 210 | 211 | const tokenSource = new vscode.CancellationTokenSource(); 212 | file.inlaysRequest = tokenSource; 213 | 214 | const request = { textDocument: { uri: file.document.uri.toString() } }; 215 | 216 | return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token) 217 | .catch(_ => null) 218 | .finally(() => { 219 | if (file.inlaysRequest === tokenSource) { 220 | file.inlaysRequest = null; 221 | } 222 | }); 223 | } 224 | } 225 | 226 | interface InlaysDecorations { 227 | type: vscode.DecorationOptions[]; 228 | param: vscode.DecorationOptions[]; 229 | chaining: vscode.DecorationOptions[]; 230 | } 231 | 232 | interface RustSourceFile { 233 | /** 234 | * Source of the token to cancel in-flight inlay hints request if any. 235 | */ 236 | inlaysRequest: null | vscode.CancellationTokenSource; 237 | /** 238 | * Last applied decorations. 239 | */ 240 | cachedDecorations: null | InlaysDecorations; 241 | 242 | document: RustDocument; 243 | } 244 | -------------------------------------------------------------------------------- /rust-analyzer/editors/code/src/client.ts: -------------------------------------------------------------------------------- 1 | import * as lc from 'vscode-languageclient'; 2 | import * as vscode from 'vscode'; 3 | import * as ra from '../src/lsp_ext'; 4 | import * as Is from 'vscode-languageclient/lib/utils/is'; 5 | 6 | import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed'; 7 | import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed'; 8 | import { assert } from './util'; 9 | 10 | function renderCommand(cmd: ra.CommandLink) { 11 | return `[${cmd.title}](command:${cmd.command}?${encodeURIComponent(JSON.stringify(cmd.arguments))} '${cmd.tooltip!}')`; 12 | } 13 | 14 | function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownString { 15 | const text = actions.map(group => 16 | (group.title ? (group.title + " ") : "") + group.commands.map(renderCommand).join(' | ') 17 | ).join('___'); 18 | 19 | const result = new vscode.MarkdownString(text); 20 | result.isTrusted = true; 21 | return result; 22 | } 23 | 24 | export function createClient(serverPath: string, cwd: string): lc.LanguageClient { 25 | // '.' Is the fallback if no folder is open 26 | // TODO?: Workspace folders support Uri's (eg: file://test.txt). 27 | // It might be a good idea to test if the uri points to a file. 28 | 29 | const run: lc.Executable = { 30 | command: serverPath, 31 | options: { cwd }, 32 | }; 33 | const serverOptions: lc.ServerOptions = { 34 | run, 35 | debug: run, 36 | }; 37 | const traceOutputChannel = vscode.window.createOutputChannel( 38 | 'Rust Analyzer Language Server Trace', 39 | ); 40 | 41 | const clientOptions: lc.LanguageClientOptions = { 42 | documentSelector: [{ scheme: 'file', language: 'rust' }], 43 | initializationOptions: vscode.workspace.getConfiguration("rust-analyzer"), 44 | traceOutputChannel, 45 | middleware: { 46 | // Workaround for https://github.com/microsoft/vscode-languageserver-node/issues/576 47 | async provideDocumentSemanticTokens(document: vscode.TextDocument, token: vscode.CancellationToken, next: DocumentSemanticsTokensSignature) { 48 | const res = await next(document, token); 49 | if (res === undefined) throw new Error('busy'); 50 | return res; 51 | }, 52 | async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, _next: lc.ProvideHoverSignature) { 53 | return client.sendRequest(lc.HoverRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token).then( 54 | (result) => { 55 | const hover = client.protocol2CodeConverter.asHover(result); 56 | if (hover) { 57 | const actions = (result).actions; 58 | if (actions) { 59 | hover.contents.push(renderHoverActions(actions)); 60 | } 61 | } 62 | return hover; 63 | }, 64 | (error) => { 65 | client.logFailedRequest(lc.HoverRequest.type, error); 66 | return Promise.resolve(null); 67 | }); 68 | }, 69 | // Using custom handling of CodeActions where each code action is resolved lazily 70 | // That's why we are not waiting for any command or edits 71 | async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) { 72 | const params: lc.CodeActionParams = { 73 | textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), 74 | range: client.code2ProtocolConverter.asRange(range), 75 | context: client.code2ProtocolConverter.asCodeActionContext(context) 76 | }; 77 | return client.sendRequest(lc.CodeActionRequest.type, params, token).then((values) => { 78 | if (values === null) return undefined; 79 | const result: (vscode.CodeAction | vscode.Command)[] = []; 80 | const groups = new Map(); 81 | for (const item of values) { 82 | // In our case we expect to get code edits only from diagnostics 83 | if (lc.CodeAction.is(item)) { 84 | assert(!item.command, "We don't expect to receive commands in CodeActions"); 85 | const action = client.protocol2CodeConverter.asCodeAction(item); 86 | result.push(action); 87 | continue; 88 | } 89 | assert(isCodeActionWithoutEditsAndCommands(item), "We don't expect edits or commands here"); 90 | const kind = client.protocol2CodeConverter.asCodeActionKind((item as any).kind); 91 | const action = new vscode.CodeAction(item.title, kind); 92 | const group = (item as any).group; 93 | const id = (item as any).id; 94 | const resolveParams: ra.ResolveCodeActionParams = { 95 | id: id, 96 | codeActionParams: params 97 | }; 98 | action.command = { 99 | command: "rust-analyzer.resolveCodeAction", 100 | title: item.title, 101 | arguments: [resolveParams], 102 | }; 103 | if (group) { 104 | let entry = groups.get(group); 105 | if (!entry) { 106 | entry = { index: result.length, items: [] }; 107 | groups.set(group, entry); 108 | result.push(action); 109 | } 110 | entry.items.push(action); 111 | } else { 112 | result.push(action); 113 | } 114 | } 115 | for (const [group, { index, items }] of groups) { 116 | if (items.length === 1) { 117 | result[index] = items[0]; 118 | } else { 119 | const action = new vscode.CodeAction(group); 120 | action.kind = items[0].kind; 121 | action.command = { 122 | command: "rust-analyzer.applyActionGroup", 123 | title: "", 124 | arguments: [items.map((item) => { 125 | return { label: item.title, arguments: item.command!!.arguments!![0] }; 126 | })], 127 | }; 128 | result[index] = action; 129 | } 130 | } 131 | return result; 132 | }, 133 | (_error) => undefined 134 | ); 135 | } 136 | 137 | } as any 138 | }; 139 | 140 | const client = new lc.LanguageClient( 141 | 'rust-analyzer', 142 | 'Rust Analyzer Language Server', 143 | serverOptions, 144 | clientOptions, 145 | ); 146 | 147 | // To turn on all proposed features use: client.registerProposedFeatures(); 148 | // Here we want to enable CallHierarchyFeature and SemanticTokensFeature 149 | // since they are available on stable. 150 | // Note that while these features are stable in vscode their LSP protocol 151 | // implementations are still in the "proposed" category for 3.16. 152 | client.registerFeature(new CallHierarchyFeature(client)); 153 | client.registerFeature(new SemanticTokensFeature(client)); 154 | client.registerFeature(new ExperimentalFeatures()); 155 | 156 | return client; 157 | } 158 | 159 | class ExperimentalFeatures implements lc.StaticFeature { 160 | fillClientCapabilities(capabilities: lc.ClientCapabilities): void { 161 | const caps: any = capabilities.experimental ?? {}; 162 | caps.snippetTextEdit = true; 163 | caps.codeActionGroup = true; 164 | caps.resolveCodeAction = true; 165 | caps.hoverActions = true; 166 | caps.statusNotification = true; 167 | capabilities.experimental = caps; 168 | } 169 | initialize(_capabilities: lc.ServerCapabilities, _documentSelector: lc.DocumentSelector | undefined): void { 170 | } 171 | } 172 | 173 | function isCodeActionWithoutEditsAndCommands(value: any): boolean { 174 | const candidate: lc.CodeAction = value; 175 | return candidate && Is.string(candidate.title) && 176 | (candidate.diagnostics === void 0 || Is.typedArray(candidate.diagnostics, lc.Diagnostic.is)) && 177 | (candidate.kind === void 0 || Is.string(candidate.kind)) && 178 | (candidate.edit === void 0 && candidate.command === void 0); 179 | } 180 | -------------------------------------------------------------------------------- /src/rls.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { promisify } from 'util'; 5 | 6 | import * as vs from 'vscode'; 7 | import * as lc from 'vscode-languageclient'; 8 | 9 | import { WorkspaceProgress } from './extension'; 10 | import { SignatureHelpProvider } from './providers/signatureHelpProvider'; 11 | import { ensureComponents, ensureToolchain, rustupUpdate } from './rustup'; 12 | import { Observable } from './utils/observable'; 13 | 14 | const exec = promisify(child_process.exec); 15 | 16 | /** Rustup components required for the RLS to work correctly. */ 17 | const REQUIRED_COMPONENTS = ['rust-analysis', 'rust-src', 'rls']; 18 | 19 | /** 20 | * VSCode settings to be observed and sent to RLS whenever they change. 21 | * Previously we just used 'rust' but since RLS warns against unrecognized 22 | * options and because we want to unify the options behind a single 'rust' 23 | * namespace for both client/server configuration, we explicitly list the 24 | * settings previously sent to the RLS. 25 | * TODO: Replace RLS' configuration setup with workspace/configuration request. 26 | */ 27 | const OBSERVED_SETTINGS = [ 28 | 'rust.sysroot', 29 | 'rust.target', 30 | 'rust.rustflags', 31 | 'rust.clear_env_rust_log', 32 | 'rust.build_lib', 33 | 'rust.build_bin', 34 | 'rust.cfg_test', 35 | 'rust.unstable_features', 36 | 'rust.wait_to_build', 37 | 'rust.show_warnings', 38 | 'rust.crate_blacklist', 39 | 'rust.build_on_save', 40 | 'rust.features', 41 | 'rust.all_features', 42 | 'rust.no_default_features', 43 | 'rust.racer_completion', 44 | 'rust.clippy_preference', 45 | 'rust.jobs', 46 | 'rust.all_targets', 47 | 'rust.target_dir', 48 | 'rust.rustfmt_path', 49 | 'rust.build_command', 50 | 'rust.full_docs', 51 | 'rust.show_hover_context', 52 | ]; 53 | 54 | /** 55 | * Parameter type to `window/progress` request as issued by the RLS. 56 | * https://github.com/rust-lang/rls/blob/17a439440e6b00b1f014a49c6cf47752ecae5bb7/rls/src/lsp_data.rs#L395-L419 57 | */ 58 | interface ProgressParams { 59 | id: string; 60 | title?: string; 61 | message?: string; 62 | percentage?: number; 63 | done?: boolean; 64 | } 65 | 66 | export function createLanguageClient( 67 | folder: vs.WorkspaceFolder, 68 | config: { 69 | updateOnStartup?: boolean; 70 | revealOutputChannelOn?: lc.RevealOutputChannelOn; 71 | logToFile?: boolean; 72 | rustup: { disabled: boolean; path: string; channel: string }; 73 | rls: { path?: string }; 74 | }, 75 | ): lc.LanguageClient { 76 | const serverOptions: lc.ServerOptions = async () => { 77 | if (config.updateOnStartup && !config.rustup.disabled) { 78 | await rustupUpdate(config.rustup); 79 | } 80 | return makeRlsProcess( 81 | config.rustup, 82 | { 83 | path: config.rls.path, 84 | cwd: folder.uri.fsPath, 85 | }, 86 | { logToFile: config.logToFile }, 87 | ); 88 | }; 89 | 90 | const clientOptions: lc.LanguageClientOptions = { 91 | // Register the server for Rust files 92 | documentSelector: [ 93 | { language: 'rust', scheme: 'untitled' }, 94 | documentFilter(folder), 95 | ], 96 | diagnosticCollectionName: `rust-${folder.uri}`, 97 | synchronize: { configurationSection: OBSERVED_SETTINGS }, 98 | // Controls when to focus the channel rather than when to reveal it in the drop-down list 99 | revealOutputChannelOn: config.revealOutputChannelOn, 100 | initializationOptions: { 101 | omitInitBuild: true, 102 | cmdRun: true, 103 | }, 104 | workspaceFolder: folder, 105 | }; 106 | 107 | return new lc.LanguageClient( 108 | 'rust-client', 109 | 'Rust Language Server', 110 | serverOptions, 111 | clientOptions, 112 | ); 113 | } 114 | 115 | export function setupClient( 116 | client: lc.LanguageClient, 117 | folder: vs.WorkspaceFolder, 118 | ): vs.Disposable[] { 119 | return [ 120 | vs.languages.registerSignatureHelpProvider( 121 | documentFilter(folder), 122 | new SignatureHelpProvider(client), 123 | '(', 124 | ',', 125 | ), 126 | ]; 127 | } 128 | 129 | export function setupProgress( 130 | client: lc.LanguageClient, 131 | observableProgress: Observable, 132 | ) { 133 | const runningProgress: Set = new Set(); 134 | // We can only register notification handler after the client is ready 135 | client.onReady().then(() => 136 | client.onNotification( 137 | new lc.NotificationType('window/progress'), 138 | progress => { 139 | if (progress.done) { 140 | runningProgress.delete(progress.id); 141 | } else { 142 | runningProgress.add(progress.id); 143 | } 144 | if (runningProgress.size) { 145 | let status = ''; 146 | if (typeof progress.percentage === 'number') { 147 | status = `${Math.round(progress.percentage * 100)}%`; 148 | } else if (progress.message) { 149 | status = progress.message; 150 | } else if (progress.title) { 151 | status = `[${progress.title.toLowerCase()}]`; 152 | } 153 | observableProgress.value = { state: 'progress', message: status }; 154 | } else { 155 | observableProgress.value = { state: 'ready' }; 156 | } 157 | }, 158 | ), 159 | ); 160 | } 161 | 162 | function documentFilter(folder: vs.WorkspaceFolder): lc.DocumentFilter { 163 | // This accepts `vscode.GlobPattern` under the hood, which requires only 164 | // forward slashes. It's worth mentioning that RelativePattern does *NOT* 165 | // work in remote scenarios (?), so rely on normalized fs path from VSCode URIs. 166 | const pattern = `${folder.uri.fsPath.replace(path.sep, '/')}/**`; 167 | 168 | return { language: 'rust', scheme: 'file', pattern }; 169 | } 170 | 171 | async function getSysroot( 172 | rustup: { disabled: boolean; path: string; channel: string }, 173 | env: typeof process.env, 174 | ): Promise { 175 | const printSysrootCmd = rustup.disabled 176 | ? 'rustc --print sysroot' 177 | : `${rustup.path} run ${rustup.channel} rustc --print sysroot`; 178 | 179 | const { stdout } = await exec(printSysrootCmd, { env }); 180 | return stdout.toString().trim(); 181 | } 182 | 183 | // Make an evironment to run the RLS. 184 | async function makeRlsEnv( 185 | rustup: { disabled: boolean; path: string; channel: string }, 186 | opts = { 187 | setLibPath: false, 188 | }, 189 | ): Promise { 190 | // Shallow clone, we don't want to modify this process' $PATH or 191 | // $(DY)LD_LIBRARY_PATH 192 | const env = { ...process.env }; 193 | 194 | let sysroot: string | undefined; 195 | try { 196 | sysroot = await getSysroot(rustup, env); 197 | } catch (err) { 198 | console.info(err.message); 199 | console.info(`Let's retry with extended $PATH`); 200 | env.PATH = `${env.HOME || '~'}/.cargo/bin:${env.PATH || ''}`; 201 | try { 202 | sysroot = await getSysroot(rustup, env); 203 | } catch (e) { 204 | console.warn('Error reading sysroot (second try)', e); 205 | vs.window.showWarningMessage(`Error reading sysroot: ${e.message}`); 206 | return env; 207 | } 208 | } 209 | 210 | console.info(`Setting sysroot to`, sysroot); 211 | if (opts.setLibPath) { 212 | const appendEnv = (envVar: string, newComponent: string) => { 213 | const old = process.env[envVar]; 214 | return old ? `${newComponent}:${old}` : newComponent; 215 | }; 216 | const newComponent = path.join(sysroot, 'lib'); 217 | env.DYLD_LIBRARY_PATH = appendEnv('DYLD_LIBRARY_PATH', newComponent); 218 | env.LD_LIBRARY_PATH = appendEnv('LD_LIBRARY_PATH', newComponent); 219 | } 220 | 221 | return env; 222 | } 223 | 224 | async function makeRlsProcess( 225 | rustup: { disabled: boolean; path: string; channel: string }, 226 | rls: { path?: string; cwd: string }, 227 | options: { logToFile?: boolean } = {}, 228 | ): Promise { 229 | // Run "rls" from the PATH unless there's an override. 230 | const rlsPath = rls.path || 'rls'; 231 | const cwd = rls.cwd; 232 | 233 | let childProcess: child_process.ChildProcess; 234 | if (rustup.disabled) { 235 | console.info(`running without rustup: ${rlsPath}`); 236 | // Set [DY]LD_LIBRARY_PATH ourselves, since that's usually done automatically 237 | // by rustup when it chooses a toolchain 238 | const env = await makeRlsEnv(rustup, { setLibPath: true }); 239 | 240 | childProcess = child_process.spawn(rlsPath, [], { 241 | env, 242 | cwd, 243 | shell: true, 244 | }); 245 | } else { 246 | console.info(`running with rustup: ${rlsPath}`); 247 | const config = rustup; 248 | 249 | await ensureToolchain(config); 250 | if (!rls.path) { 251 | // We only need a rustup-installed RLS if we weren't given a 252 | // custom RLS path. 253 | console.info('will use a rustup-installed RLS; ensuring present'); 254 | await ensureComponents(config, REQUIRED_COMPONENTS); 255 | } 256 | 257 | const env = await makeRlsEnv(rustup, { setLibPath: false }); 258 | childProcess = child_process.spawn( 259 | config.path, 260 | ['run', config.channel, rlsPath], 261 | { env, cwd, shell: true }, 262 | ); 263 | } 264 | 265 | childProcess.on('error', (err: { code?: string; message: string }) => { 266 | if (err.code === 'ENOENT') { 267 | console.error(`Could not spawn RLS: ${err.message}`); 268 | vs.window.showWarningMessage(`Could not spawn RLS: \`${err.message}\``); 269 | } 270 | }); 271 | 272 | if (options.logToFile) { 273 | const logPath = path.join(rls.cwd, `rls${Date.now()}.log`); 274 | const logStream = fs.createWriteStream(logPath, { flags: 'w+' }); 275 | childProcess.stderr?.pipe(logStream); 276 | } 277 | 278 | return childProcess; 279 | } 280 | -------------------------------------------------------------------------------- /src/rustup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file This module wraps the most commonly used rustup interface, e.g. 3 | * seeing if rustup is installed or probing for/installing the Rust toolchain 4 | * components. 5 | */ 6 | import * as child_process from 'child_process'; 7 | import * as util from 'util'; 8 | import { window } from 'vscode'; 9 | 10 | import { startSpinner, stopSpinner } from './spinner'; 11 | import { runTaskCommand } from './tasks'; 12 | 13 | const exec = util.promisify(child_process.exec); 14 | 15 | function isInstalledRegex(componentName: string): RegExp { 16 | return new RegExp(`^(${componentName}.*) \\((default|installed)\\)$`); 17 | } 18 | 19 | export interface RustupConfig { 20 | channel: string; 21 | path: string; 22 | } 23 | 24 | export async function rustupUpdate(config: RustupConfig) { 25 | startSpinner('Updating…'); 26 | 27 | try { 28 | const { stdout } = await exec(`${config.path} update`); 29 | 30 | // This test is imperfect because if the user has multiple toolchains installed, they 31 | // might have one updated and one unchanged. But I don't want to go too far down the 32 | // rabbit hole of parsing rustup's output. 33 | if (stdout.includes('unchanged')) { 34 | stopSpinner('Up to date.'); 35 | } else { 36 | stopSpinner('Up to date. Restart extension for changes to take effect.'); 37 | } 38 | } catch (e) { 39 | console.log(e); 40 | stopSpinner('An error occurred whilst trying to update.'); 41 | } 42 | } 43 | 44 | /** 45 | * Check for the user-specified toolchain (and that rustup exists). 46 | */ 47 | export async function ensureToolchain(config: RustupConfig) { 48 | if (await hasToolchain(config)) { 49 | return; 50 | } 51 | 52 | const clicked = await window.showInformationMessage( 53 | `${config.channel} toolchain not installed. Install?`, 54 | 'Yes', 55 | ); 56 | if (clicked) { 57 | await tryToInstallToolchain(config); 58 | } else { 59 | throw new Error(); 60 | } 61 | } 62 | 63 | /** 64 | * Checks for the required toolchain components and prompts the user to install 65 | * them if they're missing. 66 | */ 67 | export async function ensureComponents( 68 | config: RustupConfig, 69 | components: string[], 70 | ) { 71 | if (await hasComponents(config, components)) { 72 | return; 73 | } 74 | 75 | const clicked = await Promise.resolve( 76 | window.showInformationMessage( 77 | 'Some Rust components not installed. Install?', 78 | 'Yes', 79 | ), 80 | ); 81 | if (clicked) { 82 | await installComponents(config, components); 83 | window.showInformationMessage('Rust components successfully installed!'); 84 | } else { 85 | throw new Error(); 86 | } 87 | } 88 | 89 | async function hasToolchain({ channel, path }: RustupConfig): Promise { 90 | // In addition to a regular channel name, also handle shorthands e.g. 91 | // `stable-msvc` or `stable-x86_64-msvc` but not `stable-x86_64-pc-msvc`. 92 | const abiSuffix = ['-gnu', '-msvc'].find(abi => channel.endsWith(abi)); 93 | const [prefix, suffix] = 94 | abiSuffix && channel.split('-').length <= 3 95 | ? [channel.substr(0, channel.length - abiSuffix.length), abiSuffix] 96 | : [channel, undefined]; 97 | // Skip middle target triple components such as vendor as necessary, since 98 | // `rustup` output lists toolchains with a full target triple inside 99 | const matcher = new RegExp([prefix, suffix && `.*${suffix}`].join('')); 100 | try { 101 | const { stdout } = await exec(`${path} toolchain list`); 102 | return matcher.test(stdout); 103 | } catch (e) { 104 | console.log(e); 105 | window.showErrorMessage( 106 | 'Rustup not available. Install from https://www.rustup.rs/', 107 | ); 108 | throw e; 109 | } 110 | } 111 | 112 | async function tryToInstallToolchain(config: RustupConfig) { 113 | startSpinner('Installing toolchain…'); 114 | try { 115 | const command = config.path; 116 | const args = ['toolchain', 'install', config.channel]; 117 | await runTaskCommand({ command, args }, 'Installing toolchain…'); 118 | if (!(await hasToolchain(config))) { 119 | throw new Error(); 120 | } 121 | } catch (e) { 122 | console.log(e); 123 | window.showErrorMessage(`Could not install ${config.channel} toolchain`); 124 | stopSpinner(`Could not install toolchain`); 125 | throw e; 126 | } 127 | } 128 | 129 | /** 130 | * Returns an array of components for specified `config.channel` toolchain. 131 | * These are parsed as-is, e.g. `rustc-x86_64-unknown-linux-gnu (default)` is a 132 | * valid listed component name. 133 | */ 134 | async function listComponents(config: RustupConfig): Promise { 135 | return exec( 136 | `${config.path} component list --toolchain ${config.channel}`, 137 | ).then(({ stdout }) => 138 | stdout 139 | .toString() 140 | .replace('\r', '') 141 | .split('\n'), 142 | ); 143 | } 144 | 145 | export async function hasComponents( 146 | config: RustupConfig, 147 | components: string[], 148 | ): Promise { 149 | try { 150 | const existingComponents = await listComponents(config); 151 | 152 | return components 153 | .map(isInstalledRegex) 154 | .every(isInstalledRegex => 155 | existingComponents.some(c => isInstalledRegex.test(c)), 156 | ); 157 | } catch (e) { 158 | console.log(e); 159 | window.showErrorMessage(`Can't detect components: ${e.message}`); 160 | stopSpinner("Can't detect components"); 161 | throw e; 162 | } 163 | } 164 | 165 | export async function installComponents( 166 | config: RustupConfig, 167 | components: string[], 168 | ) { 169 | for (const component of components) { 170 | try { 171 | const command = config.path; 172 | const args = [ 173 | 'component', 174 | 'add', 175 | component, 176 | '--toolchain', 177 | config.channel, 178 | ]; 179 | await runTaskCommand({ command, args }, `Installing \`${component}\``); 180 | 181 | const isInstalled = isInstalledRegex(component); 182 | const listedComponents = await listComponents(config); 183 | if (!listedComponents.some(c => isInstalled.test(c))) { 184 | throw new Error(); 185 | } 186 | } catch (e) { 187 | stopSpinner(`Could not install component \`${component}\``); 188 | 189 | window.showErrorMessage( 190 | `Could not install component: \`${component}\`${ 191 | e.message ? `, message: ${e.message}` : '' 192 | }`, 193 | ); 194 | throw e; 195 | } 196 | } 197 | } 198 | 199 | /** 200 | * Parses given output of `rustup show` and retrieves the local active toolchain. 201 | */ 202 | export function parseActiveToolchain(rustupOutput: string): string { 203 | // There may a default entry under 'installed toolchains' section, so search 204 | // for currently active/overridden one only under 'active toolchain' section 205 | const activeToolchainsIndex = rustupOutput.search('active toolchain'); 206 | if (activeToolchainsIndex !== -1) { 207 | rustupOutput = rustupOutput.substr(activeToolchainsIndex); 208 | 209 | const matchActiveChannel = /^(\S*) \((?:default|overridden)/gm; 210 | const match = matchActiveChannel.exec(rustupOutput); 211 | if (!match) { 212 | throw new Error( 213 | `couldn't find active toolchain under 'active toolchains'`, 214 | ); 215 | } else if (matchActiveChannel.exec(rustupOutput)) { 216 | throw new Error( 217 | `multiple active toolchains found under 'active toolchains'`, 218 | ); 219 | } 220 | 221 | return match[1]; 222 | } 223 | 224 | // Try matching the third line as the active toolchain 225 | const match = /^(?:.*\r?\n){2}(\S*) \((?:default|overridden)/.exec( 226 | rustupOutput, 227 | ); 228 | if (match) { 229 | return match[1]; 230 | } 231 | 232 | throw new Error(`couldn't find active toolchains`); 233 | } 234 | 235 | export async function getVersion(config: RustupConfig): Promise { 236 | const VERSION_REGEX = /rustup ([0-9]+\.[0-9]+\.[0-9]+)/; 237 | 238 | const output = await exec(`${config.path} --version`); 239 | const versionMatch = VERSION_REGEX.exec(output.stdout.toString()); 240 | if (versionMatch && versionMatch.length >= 2) { 241 | return versionMatch[1]; 242 | } else { 243 | throw new Error("Couldn't parse rustup version"); 244 | } 245 | } 246 | 247 | /** 248 | * Returns whether Rustup is invokable and available. 249 | */ 250 | export function hasRustup(config: RustupConfig): Promise { 251 | return getVersion(config) 252 | .then(() => true) 253 | .catch(() => false); 254 | } 255 | 256 | /** 257 | * Returns active (including local overrides) toolchain, as specified by rustup. 258 | * May throw if rustup at specified path can't be executed. 259 | */ 260 | export function getActiveChannel(wsPath: string, rustupPath: string): string { 261 | // rustup info might differ depending on where it's executed 262 | // (e.g. when a toolchain is locally overriden), so executing it 263 | // under our current workspace root should give us close enough result 264 | 265 | let activeChannel; 266 | try { 267 | // `rustup show active-toolchain` is available since rustup 1.12.0 268 | activeChannel = child_process 269 | .execSync(`${rustupPath} show active-toolchain`, { 270 | cwd: wsPath, 271 | }) 272 | .toString() 273 | .trim(); 274 | // Since rustup 1.17.0 if the active toolchain is the default, we're told 275 | // by means of a " (default)" suffix, so strip that off if it's present 276 | // If on the other hand there's an override active, we'll get an 277 | // " (overridden by ...)" message instead. 278 | activeChannel = activeChannel.replace(/ \(.*\)$/, ''); 279 | } catch (e) { 280 | // Possibly an old rustup version, so try rustup show 281 | const showOutput = child_process 282 | .execSync(`${rustupPath} show`, { 283 | cwd: wsPath, 284 | }) 285 | .toString(); 286 | activeChannel = parseActiveToolchain(showOutput); 287 | } 288 | 289 | console.info(`Using active channel: ${activeChannel}`); 290 | return activeChannel; 291 | } 292 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Unreleased 2 | 3 | ### 0.7.9 - 2022-11-15 4 | 5 | * This final version marks this extension as no longer supported. 6 | Use the [rust-lang.rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) extension instead. 7 | 8 | ### 0.7.8 - 2020-05-13 9 | 10 | * Rebrand extension as RLS-agnostic 11 | * Add missing semantic token types definition 12 | 13 | ### 0.7.7 - 2020-05-13 14 | 15 | * Only synchronize relevant workspace settings for RLS 16 | * Rename configuration section to just "Rust" 17 | 18 | ### 0.7.6 - 2020-05-12 19 | 20 | * Support rust-analyzer as an alternate LSP server 21 | * Bump required VSCode version to 1.43, use language server protocol (LSP) v3.15 22 | 23 | ### 0.7.5 - 2020-05-06 24 | 25 | * Remove redundant snippets and improve usability of select ones e.g. `if let` 26 | * Accept rustup toolchain shorthands in `rust-client.channel`, e.g. `stable-gnu` or `nightly-x86_64-msvc` 27 | * Remove deprecated `rust-client.useWsl` setting (use the official 28 | [Remote - WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) extension instead) 29 | 30 | ### 0.7.4 - 2020-04-27 31 | 32 | * Add a Start/Stop the RLS command 33 | * Introduce a `rust-client.autoStartRls` (defaults to true) setting to control the auto-start 34 | behaviour when opening a relevant Rust project file 35 | * (!) Don't immediately start server instances for every already opened file 36 | * (!) Don't immediately start server instances for newly added workspace folders 37 | * Dynamically show progress only for the active client workspace 38 | * Correctly run tasks based on active text editor rather than last opened Rust file 39 | * Use smooth, universally supported spinner in the status bar ⚙️ 40 | 41 | ### 0.7.3 - 2020-04-21 42 | 43 | * Remove redundant `rust-client.nestedMultiRootConfigInOutermost` setting (originally used to work around non-multi-project limitations) 44 | * Ignore setting `rust-client.enableMultiProjectSetup` (it's always on by default) 45 | * Fix support for multiple VSCode workspaces 46 | 47 | ### 0.7.2 - 2020-04-17 48 | 49 | * Fix a bug where rustup didn't install all of the required components for the RLS 50 | * Don't warn on custom `rust-client.channel` value such as `1.39.0` in properties.json 51 | * Add a new `default` value for `rust-client.channel` (same as setting it explicitly to `null`) 52 | * Add a self-closing angular (`>`) bracket whenever opening one (`<`) has been typed 53 | * Refresh the RLS spinner 🌕 54 | * Fix project layout detection bugs on Windows when using the `enableMultiProjectSetup` option 55 | * Prevent hover with function signature from being shown when declaring the function 56 | 57 | ### 0.7.1 - 2020-04-16 58 | 59 | * Limit scope of few extension-specific settings to `machine` 60 | * Bump required VSCode to 1.36 61 | * Change `thread::spawn` snippet to activate on `thread_spawn` prefix 62 | * Use dynamic `wait_to_build` in RLS by default rather than setting it to 1500ms 63 | 64 | ### 0.7.0 - 2019-10-15 65 | 66 | * Implement support for multi-project workspace layout 🎉 67 | * Remove deprecated `rust.use_crate_blacklist` configuration entry 68 | 69 | #### Contributors 70 | This minor release was possible thanks to: 71 | * Alex Tugarev 72 | * Igor Matuszewski 73 | * Jannick Johnsen 74 | * lwshang 75 | * Nickolay Ponomarev 76 | 77 | (Generated via `git shortlog -s --no-merges 0.6.0...0.7.0 | cut -f2 | sort`) 78 | 79 | ### 0.6.3 - 2019-09-07 80 | 81 | * Fix `rust-client.channel` config type in package.json 82 | 83 | ### 0.6.2 - 2019-09-04 84 | 85 | * Deprecate `rust.use_crate_blacklist` in favor of newly added `rust.crate_blacklist` (supported by RLS 1.38) 86 | * Expand `~` in `rust-client.{rustup,rls}Path` settings 87 | * Deprecate `rust-client.useWSL` setting (use [Remote - WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) extension instead) 88 | 89 | ### 0.6.1 - 2019-04-04 90 | 91 | * Fix Cargo task auto-detection 92 | 93 | ### 0.6.0 - 2019-04-01 94 | 95 | #### Features/Changes 96 | * Implement function signature help tooltip 97 | * Updat `print(ln)` macro snippets 98 | * Introduce `rust-client.nestedMultiRootConfigInOutermost` 99 | * Show Rust toolchain/RLS component installation progress with user-visible task pane 100 | 101 | #### Fixes 102 | * Fix overriding Rustup-enabled RLS with custom `rust-client.rlsPath` setting 103 | * Fix duplicated diagnostics originating from the build tasks 104 | * Spawn RLS at the respective workspace folder 105 | * Fix `rust-client.logToFile` on Windows 106 | * Fix ``Unknown RLS configuration: `trace`` 107 | * Let Racer generate and use its `RUST_SRC_PATH` env var 108 | * Remove support for deprecated `rustDocument/{beginBuild,diagnosticsEnd}` messages 109 | * Surface and handle more erorrs wrt. RLS spawn error 110 | * Stop warning against deprecated `RLS_{PATH,ROOT}` env vars 111 | * Stop warning against deprecated `rls.toml` 112 | * Don't change `$PATH` for the VSCode process when modifying it for the RLS 113 | * Fix URI path conversion in Problems Pane on Windows 114 | 115 | #### Contributors 116 | This release was possible thanks to: 117 | * Bastian Köcher 118 | * Igor Matuszewski 119 | * John Feminella 120 | * Przemysław Pietrzkiewicz 121 | * Radu Matei 122 | * Ricardo 123 | * SoftwareApe 124 | * TheGoddessInari 125 | * angusgraham 126 | * enzovitaliy 127 | 128 | ### 0.5.4 - 2019-03-09 129 | 130 | * Fix bug due to Rustup changes in 1.17 131 | * Remove `goto_def_racer_fallback` (replaced with `racer_completion`) 132 | * Add WSL support 133 | 134 | ### 0.5.3 - 2018-12-08 135 | 136 | * Revert Cargo.toml changes 137 | 138 | ### 0.5.2 - 2018-12-07 139 | 140 | * Prefer workspace Cargo.toml to local ones 141 | 142 | ### 0.5.1 - 2018-12-06 143 | 144 | * Try harder to find Cargo.toml 145 | * Account for the `rls-preview` to `rls` component name change (and remove the `rust-client.rls-name` option) 146 | 147 | ### 0.5.0 - 2018-12-04 148 | 149 | * Added `build_command` setting 150 | * Work better without Rustup 151 | * Fix some bugs with VSCode workspaces 152 | 153 | 154 | ### 0.4.10 - 2018-08-29 155 | 156 | * Can use an external Rustfmt using `rust.rustfmt_path` option 157 | * snippets for test, derive, and cfg 158 | * fix a bug where the Rust sysroot was set to an invalid value 159 | 160 | ### 0.4.9 - 2018-07-20 161 | 162 | * Fix a bug in the `rust.clippy_preference` setting. 163 | 164 | ### 0.4.8 - 2018-07-20 165 | 166 | * Fix some Windows bugs 167 | * add the `rust.clippy_preference` setting. 168 | * Fix some Rustup/installation bugs 169 | 170 | ### 0.4.7 - 2018-07-08 171 | 172 | * Fix missing tasks in recent versions of VSCode 173 | 174 | ### 0.4.6 - 2018-07-05 175 | 176 | * Support VSCode workspaces 177 | * Code lens for running unit tests 178 | 179 | ### 0.4.5 - 2018-06-03 180 | 181 | * Undo the change to target directory default (unnecessary with Rust 1.26.1) 182 | 183 | ### 0.4.4 - 2018-05-17 184 | 185 | * Update the VSCode client library dependency 186 | * Fix the target directory 187 | 188 | ### 0.4.3 - 2018-05-14 189 | 190 | * Set the target directory default to work around a but in the stable RLS 191 | * `extern crate` snippet 192 | * remove non-workspace mode 193 | 194 | ### 0.4.2 - 2018-04-29 195 | 196 | * Added `rust-client.rlsPath` setting for easier RLS development and debugging 197 | (and deprecated the `rls.path` setting) 198 | * Bug fixes for race conditions. 199 | * Increased the default `rust.wait_to_build` time. 200 | * Updated LS client 201 | * Added `cargo bench` task 202 | * Added `rust.target_dir` and `rust.all_targets` settings 203 | 204 | 205 | ### 0.4.0 - 2018-03-04 206 | 207 | * Added `rust.racer_completion` to allow disabling racer to work around a 208 | [performance issue](https://github.com/rust-lang-nursery/rls/issues/688). 209 | * Spinner UI improvements. 210 | * Added a `cargo check` task. 211 | * The local active toolchain channel is now the default `rust-client.channel`. 212 | * Added `rust.jobs` to allow limiting the number of parallel Cargo jobs. 213 | * Added support for workspaces. 214 | * Improved startup experience when using workspaces. 215 | * Deglob is now a code action instead of a command. 216 | * Warns and no longer crashes RLS if a single file is opened instead of a 217 | folder. 218 | * Warns if Cargo.toml is not in the root of a workspace. 219 | 220 | ### 0.3.2 - 2017-11-07 221 | 222 | * Added `rust-client.rustupPath` to override rustup location. 223 | * Added properties to control enabling of Cargo features. 224 | * Fixed an issue where nightly was used instead of the configured channel. 225 | 226 | ### 0.3.1 - 2017-10-04 227 | 228 | * Bug fix in RLS detection. 229 | 230 | ### 0.3.0 - 2017-09-29 231 | 232 | * Change the default for `rust-client.rls-name` to `rls-preview` to handle the 233 | renaming of the RLS. 234 | * Remove `rust-client.showStdErr` property. 235 | 236 | ### 0.2.3 - 2017-09-21 237 | 238 | * Warns if Config.toml is missing (likely due to opening a Rust file outside a 239 | project, which previously crashed) 240 | * Automatically continue line comments 241 | * Automatically set LD_LIBRARY_PATH (only useful when not using Rustup) 242 | * Configure the toolchain and component name for the RLS 243 | * Command to restart the RLS 244 | * Better workflow around creating build tasks 245 | * A better logo - more colour! 246 | 247 | ### 0.2.2 - 2017-08-21 248 | 249 | * Highlights errors from build tasks 250 | * Find all impls 251 | * Adds cargo clean task 252 | * Auto-detect `--lib` or `--bin` 253 | * Adds an opt-out option for creating tasks.json 254 | * Add a command to update the RLS and an option to do so on startup 255 | * Deprecate `RLS_PATH` and `RLS_ROOT` env vars 256 | * Changes to the RLS: 257 | - Easier to use deglob refactoring 258 | - Debugging and troubleshooting [instructions](https://github.com/rust-lang-nursery/rls/blob/master/debugging.md) 259 | 260 | ### 0.2.1 - 2017-08-09 261 | 262 | * Fix bug installing the rls 263 | 264 | ### 0.2.0 - 2017-08-07 265 | 266 | * Unicode (fixed width) spinner 267 | * Logging and debugging options in configuration 268 | * Deglob command is in the Rust category 269 | * Don't check tests by default (still configurable) 270 | * Set `RUST_SRC_PATH` for Racer 271 | * Travis CI for the repo 272 | * Performance and robustness improvements in the RLS, support for required options 273 | here, including 274 | - blacklist large and non-very useful crates (configurable) 275 | - configure compiler data 276 | - don't error on missing options 277 | - stabilise renaming 278 | - don't crash on non-file URLs 279 | - Racer and Rustfmt updates 280 | - only use Racer for code completion (never for 'goto def', still configurable) 281 | - improve startup build/index time 282 | - handle stale compiler data better 283 | - add an option to only build/index on save (not on change) 284 | - rebuild if Cargo.toml changes 285 | 286 | ### 0.1.0 - 2017-07-17 287 | 288 | * First release 289 | -------------------------------------------------------------------------------- /src/rustAnalyzer.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { promisify } from 'util'; 5 | 6 | import * as vs from 'vscode'; 7 | import * as lc from 'vscode-languageclient'; 8 | 9 | import { WorkspaceProgress } from './extension'; 10 | import { download, fetchRelease } from './net'; 11 | import * as rustup from './rustup'; 12 | import { Observable } from './utils/observable'; 13 | 14 | const stat = promisify(fs.stat); 15 | const mkdir = promisify(fs.mkdir); 16 | const readFile = promisify(fs.readFile); 17 | const writeFile = promisify(fs.writeFile); 18 | 19 | const REQUIRED_COMPONENTS = ['rust-src']; 20 | 21 | /** Returns a path where rust-analyzer should be installed. */ 22 | function installDir(): string | undefined { 23 | if (process.platform === 'linux' || process.platform === 'darwin') { 24 | // Prefer, in this order: 25 | // 1. $XDG_BIN_HOME (proposed addition to XDG spec) 26 | // 2. $XDG_DATA_HOME/../bin/ 27 | // 3. $HOME/.local/bin/ 28 | const { HOME, XDG_DATA_HOME, XDG_BIN_HOME } = process.env; 29 | if (XDG_BIN_HOME) { 30 | return path.resolve(XDG_BIN_HOME); 31 | } 32 | 33 | const baseDir = XDG_DATA_HOME 34 | ? path.join(XDG_DATA_HOME, '..') 35 | : HOME && path.join(HOME, '.local'); 36 | return baseDir && path.resolve(path.join(baseDir, 'bin')); 37 | } else if (process.platform === 'win32') { 38 | // %LocalAppData%\rust-analyzer\ 39 | const { LocalAppData } = process.env; 40 | return ( 41 | LocalAppData && path.resolve(path.join(LocalAppData, 'rust-analyzer')) 42 | ); 43 | } 44 | 45 | return undefined; 46 | } 47 | 48 | /** Returns a path where persistent data for rust-analyzer should be installed. */ 49 | function metadataDir(): string | undefined { 50 | if (process.platform === 'linux' || process.platform === 'darwin') { 51 | // Prefer, in this order: 52 | // 1. $XDG_CONFIG_HOME/rust-analyzer 53 | // 2. $HOME/.config/rust-analyzer 54 | const { HOME, XDG_CONFIG_HOME } = process.env; 55 | const baseDir = XDG_CONFIG_HOME || (HOME && path.join(HOME, '.config')); 56 | 57 | return baseDir && path.resolve(path.join(baseDir, 'rust-analyzer')); 58 | } else if (process.platform === 'win32') { 59 | // %LocalAppData%\rust-analyzer\ 60 | const { LocalAppData } = process.env; 61 | return ( 62 | LocalAppData && path.resolve(path.join(LocalAppData, 'rust-analyzer')) 63 | ); 64 | } 65 | 66 | return undefined; 67 | } 68 | 69 | function ensureDir(path: string) { 70 | return !!path && stat(path).catch(() => mkdir(path, { recursive: true })); 71 | } 72 | 73 | interface RustAnalyzerConfig { 74 | askBeforeDownload?: boolean; 75 | package: { 76 | releaseTag: string; 77 | }; 78 | } 79 | 80 | interface Metadata { 81 | releaseTag: string; 82 | } 83 | 84 | async function readMetadata(): Promise> { 85 | const stateDir = metadataDir(); 86 | if (!stateDir) { 87 | return { kind: 'error', code: 'NotSupported' }; 88 | } 89 | 90 | const filePath = path.join(stateDir, 'metadata.json'); 91 | if (!(await stat(filePath).catch(() => false))) { 92 | return { kind: 'error', code: 'FileMissing' }; 93 | } 94 | 95 | const contents = await readFile(filePath, 'utf8'); 96 | const obj = JSON.parse(contents) as unknown; 97 | return typeof obj === 'object' ? (obj as Record) : {}; 98 | } 99 | 100 | async function writeMetadata(config: Metadata) { 101 | const stateDir = metadataDir(); 102 | if (!stateDir) { 103 | return false; 104 | } 105 | 106 | if (!(await ensureDir(stateDir))) { 107 | return false; 108 | } 109 | 110 | const filePath = path.join(stateDir, 'metadata.json'); 111 | return writeFile(filePath, JSON.stringify(config)).then(() => true); 112 | } 113 | 114 | export async function getServer({ 115 | askBeforeDownload, 116 | package: pkg, 117 | }: RustAnalyzerConfig): Promise { 118 | let binaryName: string | undefined; 119 | if (process.arch === 'x64' || process.arch === 'ia32') { 120 | if (process.platform === 'linux') { 121 | binaryName = 'rust-analyzer-linux'; 122 | } 123 | if (process.platform === 'darwin') { 124 | binaryName = 'rust-analyzer-mac'; 125 | } 126 | if (process.platform === 'win32') { 127 | binaryName = 'rust-analyzer-windows.exe'; 128 | } 129 | } 130 | if (binaryName === undefined) { 131 | vs.window.showErrorMessage( 132 | "Unfortunately we don't ship binaries for your platform yet. " + 133 | 'You need to manually clone rust-analyzer repository and ' + 134 | 'run `cargo xtask install --server` to build the language server from sources. ' + 135 | 'If you feel that your platform should be supported, please create an issue ' + 136 | 'about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we ' + 137 | 'will consider it.', 138 | ); 139 | return undefined; 140 | } 141 | 142 | const dir = installDir(); 143 | if (!dir) { 144 | return; 145 | } 146 | await ensureDir(dir); 147 | 148 | const metadata: Partial = await readMetadata().catch(() => ({})); 149 | 150 | const dest = path.join(dir, binaryName); 151 | const exists = await stat(dest).catch(() => false); 152 | if (exists && metadata.releaseTag === pkg.releaseTag) { 153 | return dest; 154 | } 155 | 156 | if (askBeforeDownload) { 157 | const userResponse = await vs.window.showInformationMessage( 158 | `${ 159 | metadata.releaseTag && metadata.releaseTag !== pkg.releaseTag 160 | ? `You seem to have installed release \`${metadata.releaseTag}\` but requested a different one.` 161 | : '' 162 | } 163 | Release \`${pkg.releaseTag}\` of rust-analyzer is not installed.\n 164 | Install to ${dir}?`, 165 | 'Download', 166 | ); 167 | if (userResponse !== 'Download') { 168 | return dest; 169 | } 170 | } 171 | 172 | const release = await fetchRelease( 173 | 'rust-analyzer', 174 | 'rust-analyzer', 175 | pkg.releaseTag, 176 | ); 177 | const artifact = release.assets.find(asset => asset.name === binaryName); 178 | if (!artifact) { 179 | throw new Error(`Bad release: ${JSON.stringify(release)}`); 180 | } 181 | 182 | await download( 183 | artifact.browser_download_url, 184 | dest, 185 | 'Downloading rust-analyzer server', 186 | { mode: 0o755 }, 187 | ); 188 | 189 | await writeMetadata({ releaseTag: pkg.releaseTag }).catch(() => { 190 | vs.window.showWarningMessage(`Couldn't save rust-analyzer metadata`); 191 | }); 192 | 193 | return dest; 194 | } 195 | 196 | /** 197 | * Rust Analyzer does not work in an isolated environment and greedily analyzes 198 | * the workspaces itself, so make sure to spawn only a single instance. 199 | */ 200 | let INSTANCE: lc.LanguageClient | undefined; 201 | 202 | /** 203 | * TODO: 204 | * Global observable progress 205 | */ 206 | const PROGRESS: Observable = new Observable< 207 | WorkspaceProgress 208 | >({ state: 'standby' }); 209 | 210 | export async function createLanguageClient( 211 | folder: vs.WorkspaceFolder, 212 | config: { 213 | revealOutputChannelOn?: lc.RevealOutputChannelOn; 214 | logToFile?: boolean; 215 | rustup: { disabled: boolean; path: string; channel: string }; 216 | rustAnalyzer: { path?: string; releaseTag: string }; 217 | }, 218 | ): Promise { 219 | if (!config.rustup.disabled) { 220 | await rustup.ensureToolchain(config.rustup); 221 | await rustup.ensureComponents(config.rustup, REQUIRED_COMPONENTS); 222 | } 223 | 224 | if (!config.rustAnalyzer.path) { 225 | await getServer({ 226 | askBeforeDownload: true, 227 | package: { releaseTag: config.rustAnalyzer.releaseTag }, 228 | }); 229 | } 230 | 231 | if (INSTANCE) { 232 | return INSTANCE; 233 | } 234 | 235 | const serverOptions: lc.ServerOptions = async () => { 236 | const binPath = 237 | config.rustAnalyzer.path || 238 | (await getServer({ 239 | package: { releaseTag: config.rustAnalyzer.releaseTag }, 240 | })); 241 | 242 | if (!binPath) { 243 | throw new Error("Couldn't fetch Rust Analyzer binary"); 244 | } 245 | 246 | const childProcess = child_process.exec(binPath); 247 | if (config.logToFile) { 248 | const logPath = path.join(folder.uri.fsPath, `ra-${Date.now()}.log`); 249 | const logStream = fs.createWriteStream(logPath, { flags: 'w+' }); 250 | childProcess.stderr?.pipe(logStream); 251 | } 252 | 253 | return childProcess; 254 | }; 255 | 256 | const clientOptions: lc.LanguageClientOptions = { 257 | // Register the server for Rust files 258 | documentSelector: [ 259 | { language: 'rust', scheme: 'file' }, 260 | { language: 'rust', scheme: 'untitled' }, 261 | ], 262 | diagnosticCollectionName: `rust`, 263 | // synchronize: { configurationSection: 'rust' }, 264 | // Controls when to focus the channel rather than when to reveal it in the drop-down list 265 | revealOutputChannelOn: config.revealOutputChannelOn, 266 | // TODO: Support and type out supported settings by the rust-analyzer 267 | initializationOptions: vs.workspace.getConfiguration('rust.rust-analyzer'), 268 | }; 269 | 270 | INSTANCE = new lc.LanguageClient( 271 | 'rust-client', 272 | 'Rust Analyzer', 273 | serverOptions, 274 | clientOptions, 275 | ); 276 | 277 | // Enable semantic highlighting which is available in stable VSCode 278 | INSTANCE.registerProposedFeatures(); 279 | // We can install only one progress handler so make sure to do that when 280 | // setting up the singleton instance 281 | setupGlobalProgress(INSTANCE); 282 | 283 | return INSTANCE; 284 | } 285 | 286 | async function setupGlobalProgress(client: lc.LanguageClient) { 287 | client.onDidChangeState(async ({ newState }) => { 288 | if (newState === lc.State.Starting) { 289 | await client.onReady(); 290 | 291 | const RUST_ANALYZER_PROGRESS = 'rustAnalyzer/roots scanned'; 292 | client.onProgress( 293 | new lc.ProgressType<{ 294 | kind: 'begin' | 'report' | 'end'; 295 | message?: string; 296 | }>(), 297 | RUST_ANALYZER_PROGRESS, 298 | ({ kind, message: msg }) => { 299 | if (kind === 'report') { 300 | PROGRESS.value = { state: 'progress', message: msg || '' }; 301 | } 302 | if (kind === 'end') { 303 | PROGRESS.value = { state: 'ready' }; 304 | } 305 | }, 306 | ); 307 | } 308 | }); 309 | } 310 | 311 | export function setupClient( 312 | _client: lc.LanguageClient, 313 | _folder: vs.WorkspaceFolder, 314 | ): vs.Disposable[] { 315 | return []; 316 | } 317 | 318 | export function setupProgress( 319 | _client: lc.LanguageClient, 320 | workspaceProgress: Observable, 321 | ) { 322 | workspaceProgress.value = PROGRESS.value; 323 | // We can only ever install one progress handler per language client and since 324 | // we can only ever have one instance of Rust Analyzer, fake the global 325 | // progress as a workspace one. 326 | PROGRESS.observe(progress => { 327 | workspaceProgress.value = progress; 328 | }); 329 | } 330 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { 2 | commands, 3 | ConfigurationTarget, 4 | Disposable, 5 | ExtensionContext, 6 | IndentAction, 7 | languages, 8 | TextEditor, 9 | Uri, 10 | window, 11 | workspace, 12 | WorkspaceFolder, 13 | WorkspaceFoldersChangeEvent, 14 | } from 'vscode'; 15 | import * as lc from 'vscode-languageclient'; 16 | 17 | import { RLSConfiguration } from './configuration'; 18 | import * as rls from './rls'; 19 | import * as rustAnalyzer from './rustAnalyzer'; 20 | import { rustupUpdate } from './rustup'; 21 | import { startSpinner, stopSpinner } from './spinner'; 22 | import { activateTaskProvider, Execution, runRlsCommand } from './tasks'; 23 | import { Observable } from './utils/observable'; 24 | import { nearestParentWorkspace } from './utils/workspace'; 25 | 26 | /** 27 | * External API as exposed by the extension. Can be queried by other extensions 28 | * or by the integration test runner for VSCode extensions. 29 | */ 30 | export interface Api { 31 | activeWorkspace: typeof activeWorkspace; 32 | } 33 | 34 | export async function activate(context: ExtensionContext): Promise { 35 | context.subscriptions.push( 36 | ...[ 37 | configureLanguage(), 38 | ...registerCommands(), 39 | workspace.onDidChangeWorkspaceFolders(whenChangingWorkspaceFolders), 40 | window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), 41 | ], 42 | ); 43 | // Manually trigger the first event to start up server instance if necessary, 44 | // since VSCode doesn't do that on startup by itself. 45 | onDidChangeActiveTextEditor(window.activeTextEditor); 46 | 47 | const config = workspace.getConfiguration(); 48 | if (!config.get('rust.ignore_deprecation_warning', false)) { 49 | window 50 | .showWarningMessage( 51 | 'rust-lang.rust has been deprecated. Please uninstall this extension and install rust-lang.rust-analyzer instead. You can find the extension by clicking on one of the buttons', 52 | 'Open in your browser', 53 | 'Open in a new editor tab', 54 | 'Disable Warning', 55 | ) 56 | .then(button => { 57 | switch (button) { 58 | case 'Disable Warning': 59 | config.update( 60 | 'rust.ignore_deprecation_warning', 61 | true, 62 | ConfigurationTarget.Global, 63 | ); 64 | break; 65 | case 'Open in your browser': 66 | commands.executeCommand( 67 | 'vscode.open', 68 | Uri.parse( 69 | 'https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer', 70 | ), 71 | ); 72 | break; 73 | case 'Open in a new editor tab': 74 | commands.executeCommand( 75 | 'vscode.open', 76 | Uri.parse('vscode:extension/rust-lang.rust-analyzer'), 77 | ); 78 | break; 79 | default: 80 | } 81 | }); 82 | } 83 | 84 | // Migrate the users of multi-project setup for RLS to disable the setting 85 | // entirely (it's always on now) 86 | if ( 87 | typeof config.get( 88 | 'rust-client.enableMultiProjectSetup', 89 | null, 90 | ) === 'boolean' 91 | ) { 92 | window 93 | .showWarningMessage( 94 | 'The multi-project setup for RLS is always enabled, so the `rust-client.enableMultiProjectSetup` setting is now redundant', 95 | { modal: false }, 96 | { title: 'Remove' }, 97 | ) 98 | .then(value => { 99 | if (value && value.title === 'Remove') { 100 | return config.update( 101 | 'rust-client.enableMultiProjectSetup', 102 | null, 103 | ConfigurationTarget.Global, 104 | ); 105 | } 106 | return; 107 | }); 108 | } 109 | 110 | return { activeWorkspace }; 111 | } 112 | 113 | export async function deactivate() { 114 | return Promise.all([...workspaces.values()].map(ws => ws.stop())); 115 | } 116 | 117 | /** Tracks dynamically updated progress for the active client workspace for UI purposes. */ 118 | let progressObserver: Disposable | undefined; 119 | 120 | function onDidChangeActiveTextEditor(editor: TextEditor | undefined) { 121 | if (!editor || !editor.document) { 122 | return; 123 | } 124 | const { languageId, uri } = editor.document; 125 | 126 | const workspace = clientWorkspaceForUri(uri, { 127 | initializeIfMissing: languageId === 'rust' || languageId === 'toml', 128 | }); 129 | if (!workspace) { 130 | return; 131 | } 132 | 133 | activeWorkspace.value = workspace; 134 | 135 | const updateProgress = (progress: WorkspaceProgress) => { 136 | if (progress.state === 'progress') { 137 | startSpinner(`[${workspace.folder.name}] ${progress.message}`); 138 | } else { 139 | const readySymbol = 140 | progress.state === 'standby' ? '$(debug-stop)' : '$(debug-start)'; 141 | stopSpinner(`[${workspace.folder.name}] ${readySymbol}`); 142 | } 143 | }; 144 | 145 | if (progressObserver) { 146 | progressObserver.dispose(); 147 | } 148 | progressObserver = workspace.progress.observe(updateProgress); 149 | // Update UI ourselves immediately and don't wait for value update callbacks 150 | updateProgress(workspace.progress.value); 151 | } 152 | 153 | function whenChangingWorkspaceFolders(e: WorkspaceFoldersChangeEvent) { 154 | // If a workspace is removed which is a Rust workspace, kill the client. 155 | for (const folder of e.removed) { 156 | const ws = workspaces.get(folder.uri.toString()); 157 | if (ws) { 158 | workspaces.delete(folder.uri.toString()); 159 | ws.stop(); 160 | } 161 | } 162 | } 163 | 164 | // Don't use URI as it's unreliable the same path might not become the same URI. 165 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 166 | const workspaces: Map = new Map(); 167 | 168 | /** 169 | * Fetches a `ClientWorkspace` for a given URI. If missing and `initializeIfMissing` 170 | * option was provided, it is additionally initialized beforehand, if applicable. 171 | */ 172 | function clientWorkspaceForUri( 173 | uri: Uri, 174 | options?: { initializeIfMissing: boolean }, 175 | ): ClientWorkspace | undefined { 176 | const rootFolder = workspace.getWorkspaceFolder(uri); 177 | if (!rootFolder) { 178 | return; 179 | } 180 | 181 | const folder = nearestParentWorkspace(rootFolder, uri.fsPath); 182 | if (!folder) { 183 | return undefined; 184 | } 185 | 186 | const existing = workspaces.get(folder.uri.toString()); 187 | if (!existing && options && options.initializeIfMissing) { 188 | const workspace = new ClientWorkspace(folder); 189 | workspaces.set(folder.uri.toString(), workspace); 190 | workspace.autoStart(); 191 | } 192 | 193 | return workspaces.get(folder.uri.toString()); 194 | } 195 | 196 | /** Denotes the state or progress the workspace is currently in. */ 197 | export type WorkspaceProgress = 198 | | { state: 'progress'; message: string } 199 | | { state: 'ready' | 'standby' }; 200 | 201 | // We run a single server/client pair per workspace folder (VSCode workspace, 202 | // not Cargo workspace). This class contains all the per-client and 203 | // per-workspace stuff. 204 | export class ClientWorkspace { 205 | public readonly folder: WorkspaceFolder; 206 | // FIXME(#233): Don't only rely on lazily initializing it once on startup, 207 | // handle possible `rust-client.*` value changes while extension is running 208 | private readonly config: RLSConfiguration; 209 | private lc: lc.LanguageClient | null = null; 210 | private disposables: Disposable[]; 211 | private _progress: Observable; 212 | get progress() { 213 | return this._progress; 214 | } 215 | 216 | constructor(folder: WorkspaceFolder) { 217 | this.config = RLSConfiguration.loadFromWorkspace(folder.uri.fsPath); 218 | this.folder = folder; 219 | this.disposables = []; 220 | this._progress = new Observable({ state: 'standby' }); 221 | } 222 | 223 | /** 224 | * Attempts to start a server instance, if not configured otherwise via 225 | * applicable `rust-client.autoStartRls` setting. 226 | * 227 | * @returns whether the server has started. 228 | */ 229 | public async autoStart() { 230 | return this.config.autoStartRls && this.start().then(() => true); 231 | } 232 | 233 | public async start() { 234 | const { createLanguageClient, setupClient, setupProgress } = 235 | this.config.engine === 'rls' ? rls : rustAnalyzer; 236 | 237 | const client = await createLanguageClient(this.folder, { 238 | updateOnStartup: this.config.updateOnStartup, 239 | revealOutputChannelOn: this.config.revealOutputChannelOn, 240 | logToFile: this.config.logToFile, 241 | rustup: { 242 | channel: this.config.channel, 243 | path: this.config.rustupPath, 244 | disabled: this.config.rustupDisabled, 245 | }, 246 | rls: { path: this.config.rlsPath }, 247 | rustAnalyzer: this.config.rustAnalyzer, 248 | }); 249 | 250 | client.onDidChangeState(({ newState }) => { 251 | if (newState === lc.State.Starting) { 252 | this._progress.value = { state: 'progress', message: 'Starting' }; 253 | } 254 | if (newState === lc.State.Stopped) { 255 | this._progress.value = { state: 'standby' }; 256 | } 257 | }); 258 | 259 | setupProgress(client, this._progress); 260 | 261 | this.disposables.push(activateTaskProvider(this.folder)); 262 | this.disposables.push(...setupClient(client, this.folder)); 263 | if (client.needsStart()) { 264 | this.disposables.push(client.start()); 265 | } 266 | } 267 | 268 | public async stop() { 269 | if (this.lc) { 270 | await this.lc.stop(); 271 | } 272 | 273 | this.disposables.forEach(d => void d.dispose()); 274 | } 275 | 276 | public async restart() { 277 | await this.stop(); 278 | return this.start(); 279 | } 280 | 281 | public runRlsCommand(cmd: Execution) { 282 | return runRlsCommand(this.folder, cmd); 283 | } 284 | 285 | public rustupUpdate() { 286 | return rustupUpdate(this.config.rustupConfig()); 287 | } 288 | } 289 | 290 | /** 291 | * Tracks the most current VSCode workspace as opened by the user. Used by the 292 | * commands to know in which workspace these should be executed. 293 | */ 294 | const activeWorkspace = new Observable(null); 295 | 296 | /** 297 | * Registers the VSCode [commands] used by the extension. 298 | * 299 | * [commands]: https://code.visualstudio.com/api/extension-guides/command 300 | */ 301 | function registerCommands(): Disposable[] { 302 | return [ 303 | commands.registerCommand('rls.update', () => 304 | activeWorkspace.value?.rustupUpdate(), 305 | ), 306 | commands.registerCommand('rls.restart', async () => 307 | activeWorkspace.value?.restart(), 308 | ), 309 | commands.registerCommand('rls.run', (cmd: Execution) => 310 | activeWorkspace.value?.runRlsCommand(cmd), 311 | ), 312 | commands.registerCommand('rls.start', () => activeWorkspace.value?.start()), 313 | commands.registerCommand('rls.stop', () => activeWorkspace.value?.stop()), 314 | ]; 315 | } 316 | 317 | /** 318 | * Sets up additional language configuration that's impossible to do via a 319 | * separate language-configuration.json file. See [1] for more information. 320 | * 321 | * [1]: https://github.com/Microsoft/vscode/issues/11514#issuecomment-244707076 322 | */ 323 | function configureLanguage(): Disposable { 324 | return languages.setLanguageConfiguration('rust', { 325 | onEnterRules: [ 326 | { 327 | // Doc single-line comment 328 | // e.g. ///| 329 | beforeText: /^\s*\/{3}.*$/, 330 | action: { indentAction: IndentAction.None, appendText: '/// ' }, 331 | }, 332 | { 333 | // Parent doc single-line comment 334 | // e.g. //!| 335 | beforeText: /^\s*\/{2}\!.*$/, 336 | action: { indentAction: IndentAction.None, appendText: '//! ' }, 337 | }, 338 | { 339 | // Begins an auto-closed multi-line comment (standard or parent doc) 340 | // e.g. /** | */ or /*! | */ 341 | beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/, 342 | afterText: /^\s*\*\/$/, 343 | action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' }, 344 | }, 345 | { 346 | // Begins a multi-line comment (standard or parent doc) 347 | // e.g. /** ...| or /*! ...| 348 | beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/, 349 | action: { indentAction: IndentAction.None, appendText: ' * ' }, 350 | }, 351 | { 352 | // Continues a multi-line comment 353 | // e.g. * ...| 354 | beforeText: /^(\ \ )*\ \*(\ ([^\*]|\*(?!\/))*)?$/, 355 | action: { indentAction: IndentAction.None, appendText: '* ' }, 356 | }, 357 | { 358 | // Dedents after closing a multi-line comment 359 | // e.g. */| 360 | beforeText: /^(\ \ )*\ \*\/\s*$/, 361 | action: { indentAction: IndentAction.None, removeText: 1 }, 362 | }, 363 | ], 364 | }); 365 | } 366 | --------------------------------------------------------------------------------