├── .codeclimate.yml ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── doc ├── cargo_command_execution.md ├── changing_configuration_parameters.md ├── common_configuration_parameters.md ├── debugging.md ├── format.md ├── install_extension_from_source.md ├── legacy_mode │ ├── linting.md │ ├── main.md │ ├── racer_configuration.md │ ├── rustfmt_configuration.md │ └── rustsym_configuration.md ├── linting.md ├── main.md ├── playground_creation.md └── rls_mode │ ├── linting.md │ └── main.md ├── gulpfile.js ├── images ├── icon.png └── linting │ ├── code.jpg │ ├── code_hover_legacy_mode.jpg │ ├── problems_panel.jpg │ └── problems_panel_legacy_mode.jpg ├── package.json ├── snippets └── rust.json ├── src ├── CargoInvocationManager.ts ├── CommandLine.ts ├── ConfigurationParameter.ts ├── IShellProvider.ts ├── NotWindowsShellProvider.ts ├── OutputChannelProcess.ts ├── OutputtingProcess.ts ├── Shell.ts ├── ShellProviderManager.ts ├── Toolchain.ts ├── UserInteraction │ ├── AskUserToAnswerYesOrNo.ts │ └── AskUserWhatConfigurationToSaveParameterIn.ts ├── UserOrWorkspaceConfiguration.ts ├── Utils.ts ├── WindowsShellProvider.ts ├── WslShellUtils.ts ├── components │ ├── cargo │ │ ├── BuildType.ts │ │ ├── CargoManager.ts │ │ ├── CargoTaskManager.ts │ │ ├── CheckTarget.ts │ │ ├── CommandInvocationReason.ts │ │ ├── CrateType.ts │ │ ├── UserDefinedArgs.ts │ │ ├── custom_configuration_chooser.ts │ │ ├── diagnostic_parser.ts │ │ ├── diagnostic_utils.ts │ │ ├── file_diagnostic.ts │ │ ├── helper.ts │ │ ├── output_channel_task_manager.ts │ │ ├── output_channel_task_status_bar_item.ts │ │ ├── output_channel_wrapper.ts │ │ ├── task.ts │ │ └── terminal_task_manager.ts │ ├── completion │ │ ├── completion_manager.ts │ │ └── racer_status_bar_item.ts │ ├── configuration │ │ ├── Configuration.ts │ │ ├── NotRustup.ts │ │ ├── RlsConfiguration.ts │ │ ├── RustSource.ts │ │ ├── Rustup.ts │ │ ├── current_working_directory_manager.ts │ │ └── mod.ts │ ├── file_system │ │ └── FileSystem.ts │ ├── formatting │ │ └── formatting_manager.ts │ ├── language_client │ │ ├── creator.ts │ │ ├── manager.ts │ │ └── status_bar_item.ts │ ├── logging │ │ ├── ILogger.ts │ │ ├── captured_message.ts │ │ ├── child_logger.ts │ │ ├── logger.ts │ │ ├── logging_manager.ts │ │ └── root_logger.ts │ ├── symbol_provision │ │ ├── document_symbol_provision_manager.ts │ │ ├── symbol_information_parser.ts │ │ ├── symbol_search_manager.ts │ │ └── workspace_symbol_provision_manager.ts │ └── tools_installation │ │ ├── installator.ts │ │ ├── missing_tools_status_bar_item.ts │ │ └── mod.ts ├── extension.ts └── legacy_mode_manager.ts ├── test ├── CommandLine.test.ts ├── OutputtingProcess.test.ts ├── Shell.test.ts ├── WslShellUtils.test.ts ├── components │ └── cargo │ │ └── diagnostic_utils.test.ts ├── index.js └── logging.test.ts ├── tsconfig.json ├── tslint.json └── typings ├── elegant-spinner └── elegant-spinner.d.ts ├── expand-tilde └── expand-tilde.d.ts ├── find-up └── find-up.d.ts ├── tmp └── tmp.d.ts └── tree-kill └── tree-kill.d.ts /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | TypeScript: true 3 | exclude_paths: 4 | - out/**/* 5 | - .vscode/* 6 | - images/* 7 | - typings/**/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | npm-*.log 4 | vsc-extension-quickstart.md 5 | .DS_Store 6 | *.vsix 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.0" 4 | 5 | script: 6 | - node node_modules/gulp/bin/gulp.js 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}" 12 | ], 13 | "stopOnEntry": true, 14 | "sourceMaps": true, 15 | "outFiles": [ 16 | "${workspaceRoot}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "npm" 19 | }, 20 | { 21 | "name": "Launch Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceRoot}", 27 | "--extensionTestsPath=${workspaceRoot}/out/test" 28 | ], 29 | "stopOnEntry": false, 30 | "sourceMaps": true, 31 | "outFiles": [ 32 | "${workspaceRoot}/out/**/*.js" 33 | ], 34 | "preLaunchTask": "npm" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 10 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | // show the output window only if unrecognized errors occur. 20 | "showOutput": "silent", 21 | 22 | // we run the custom script "compile" as defined in package.json 23 | "args": ["run", "compile", "--loglevel", "silent"], 24 | 25 | // The tsc compiler is started in watching mode 26 | "isWatching": true, 27 | 28 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 29 | "problemMatcher": "$tsc-watch" 30 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | vsc-extension-quickstart.md 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Depending on the type of contribution you wish to make, you will find there are several sections to choose from. 2 | 3 | For each section, follow the instruction as detailed below. 4 | 5 | # Open an issue 6 | 7 | Use this section if you want to open an issue describing a problem. 8 | 9 | Please follow the instructions: 10 | 11 | * Check that there is no existing open issue with the same problem. 12 | * Create a new the issue filling out the all details in the template. 13 | 14 | # Fix an open issue 15 | 16 | Use this section if you want to fix a problem or implement a feature for an open issue. 17 | 18 | Please follow the instructions: 19 | 20 | * Choose the issue. 21 | * Ensure the issue is not already being worked on. 22 | * Claim the issue by writing a comment (or something similar) like "I will work on this". 23 | * Work on said fix or feature. 24 | * Follow the instructions from the section "To be done before submitting a pull request" 25 | * Open a pull request. 26 | * In the pull request's description describe what is exactly done. 27 | * **Important**: the last line of the pull request should be: "Fixed #{number of the issue}" 28 | * Wait until the pull request is merged 29 | 30 | # Propose fix a bug or add a feature 31 | 32 | Use this section if you want to fix a problem or add a feature, but there is no existing open issue for it. 33 | 34 | Please follow the instructions: 35 | 36 | * Create a new issue and describe your proposal in detail. 37 | * If you are unsure about implementation, discuss it in the issue. 38 | * Follow the instructions from the section "Fix an open issue" 39 | 40 | # Suggest a feature or change behavior 41 | 42 | Use this section if you have an idea, but you are unsure whether other people would like it. 43 | 44 | Please follow the instructions: 45 | 46 | * Create a new issue and describe your proposal in detail. 47 | * Mark it with the "Question" label. 48 | * Discuss it 49 | 50 | # To be done before submitting a pull request 51 | 52 | * Execute `npm run gulp` in the root directory of the extension to compile the code and to check for code style violation 53 | * Launch tests: 54 | * Open the project in VSCode 55 | * Switch to Debug 56 | * Launch tests 57 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Version of VSCode: 2 | Version of the extension: 3 | OS: 4 | 5 | Description: 6 | 7 | Output of the "Rust logging" channel: 8 | ``` 9 | 10 | ``` 11 | 12 | Remove everything below before submitting an issue. 13 | 14 | To see the "Rust logging" channel perform the following steps: 15 | 16 | * Click on the "View" menu item 17 | * Click on the "Output" menu item 18 | * The "Output" panel should open 19 | * There should be a select box to the right of the "Output" panel 20 | * Click on the select box 21 | * Choose "Rust Logging" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Constantine Akhantyev 4 | Copyright (c) 2016 Kalita Alexey 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://api.travis-ci.org/editor-rs/vscode-rust.svg)](https://travis-ci.org/editor-rs/vscode-rust) 2 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/vscode-rust/Lobby) 3 | 4 | **Hello everyone. I'm a maintainer and I'm very busy. At some point rls-vscode will supersede the extension. Until that moment someone should maintain the extension. If you are interested in it, please send me an email to kalita.alexey@outlook.com** 5 | 6 | # Rust for Visual Studio Code (Latest: 0.4.2) 7 | 8 | ## What is the repository? 9 | 10 | The extension is continuation of RustyCode (an extension for Visual Studio Code for Rust language). 11 | 12 | RustyCode is no longer maintained and the developer seems to have lost all interest in the extension. Due to no response from the original author, this repository was created which now contains additional features and many bug fixes. 13 | 14 | ## Extension 15 | 16 | - [Documentation](doc/main.md) 17 | - [Contributing](CONTRIBUTING.md) 18 | 19 | This extension adds advanced language support for the Rust programming language within VS Code. It features: 20 | 21 | - [Rust Language Server](https://github.com/rust-lang-nursery/rls) integration. 22 | - Autocompletion (via `racer` or RLS). 23 | - Go To Definition (via `racer` or RLS). 24 | - Go To Symbol (via `rustsym` or RLS). 25 | - Code formatting (via `rustfmt`). 26 | - Code Snippets. 27 | - Cargo tasks (Ctrl+Shift+P and type `cargo` to view them). 28 | - …and a lot of other features. To learn more, see the [documentation](doc/main.md). 29 | 30 | On update, please review the [changelog](CHANGELOG.md). 31 | 32 | ## Installation 33 | 34 | 1. Firstly, you will need to install [VS Code](https://code.visualstudio.com/) `1.8` or later. 35 | 36 | 2. Now in VS Code, Ctrl+P and type `ext install vscode-rust`. 37 | 38 | 3. Choose to install the "Rust" extension. 39 | 40 | The extension can also be found on the [VS Code marketplace](https://marketplace.visualstudio.com/items?itemName=kalitaalexey.vscode-rust). 41 | 42 | ## License 43 | 44 | [MIT](LICENSE) 45 | -------------------------------------------------------------------------------- /doc/cargo_command_execution.md: -------------------------------------------------------------------------------- 1 | # Cargo Command Execution Page 2 | 3 | The extension allows a developer to execute any of the inbuilt Cargo commands. 4 | 5 | These commands are: 6 | 7 | * `bench` 8 | * `build` 9 | * `check` 10 | * `clean` 11 | * `clippy` 12 | * `doc` 13 | * `new` 14 | * `run` 15 | * `test` 16 | * `update` 17 | 18 | These commands are available through the command palette (Ctrl+Shift+P) and have the prefix `"Cargo: "`. 19 | 20 | ## Execute Command On Save 21 | 22 | The extension supports executing some of these commands after saving the active document. 23 | 24 | The `"rust.actionOnSave"` configuration parameter specifies which command to execute. 25 | 26 | The possible values are: 27 | 28 | * `"build"` - executes `"Cargo: Build"` 29 | * `"check"` - executes `"Cargo: Check"` 30 | * `"clippy"` - executes `"Cargo: Clippy"` 31 | * `"doc"` - executes `"Cargo: Doc"` 32 | * `"run"` - executes `"Cargo: Run"` 33 | * `"test"` - executes `"Cargo: Test"` 34 | * `null` - the extension does nothing (default) 35 | 36 | ## Finding Out Cargo.toml 37 | 38 | Before executing the command, the extension needs to find out which `Cargo.toml` to use. The extension uses the following algorithm: 39 | 40 | * Try to determine the current working directory from the active text editor 41 | 42 | If all of the following conditions are met: 43 | 44 | * There is an active text editor 45 | * A file opened in the editor is within the workspace (the directory opened in VS Code) 46 | * There is a `Cargo.toml` in the same directory as the active file or in any of the parent directories within the workspace 47 | 48 | Then use the `Cargo.toml` file that was found. 49 | 50 | * Try using the previous `Cargo.toml` file 51 | * Try using the `Cargo.toml` from the workspace 52 | 53 | If the extension fails to find a `Cargo.toml`, an error message is shown. 54 | 55 | ## Finding Out The Working Directory 56 | 57 | Before executing a Cargo command, the extension must find out which directory to execute the command in. 58 | 59 | The extension supports the `"rust.cargoCwd"` configuration parameter with the following possible values: 60 | 61 | * `"/some/path"` - the extension uses the specified path as the command's working directory 62 | * `null` - the directory containing the chosen `Cargo.toml` is used as Cargo's working directory (default `cargo` behavior) 63 | 64 | ## Configuration Parameters 65 | 66 | ### Cargo Path 67 | 68 | The `"rust.cargoPath"` configuration parameter specifies a path to the `cargo` executable with the following possible values: 69 | 70 | * `"/some/path"` - the extension would try to use the path 71 | * `null` - the extension would try to use `cargo` from the `PATH` environment variable. 72 | 73 | If `cargo` isn't available, the extension can't execute any Cargo commands. 74 | 75 | ### Cargo Environment 76 | 77 | The `"rust.cargoEnv"` configuration parameter specifies an environment variable which would be added to the general environment when executing a Cargo command. 78 | 79 | The possible values are: 80 | 81 | * `{ "Some": object }` 82 | * `null` 83 | 84 | #### Examples 85 | 86 | ```json 87 | "rust.cargoEnv": { "RUST_BACKTRACE": 1 } 88 | ``` 89 | 90 | ### Executing Cargo commands in an integrated terminal 91 | 92 | The `"rust.executeCargoCommandInTerminal"` configuration parameter controls whether a Cargo command should be executed in an integrated terminal. 93 | 94 | By default, the extension executes Cargo commands as child processes. It then parses the output of the command and publishes diagnostics. Executing Cargo commands in an integrated terminal is useful if you need to run a binary and enter some text. 95 | 96 | Unfortunately, there is currently no way to parse output of an integrated terminal. This means diagnostics cannot be shown in the editor. 97 | 98 | The configuration parameter supports the following values: 99 | 100 | * `true` - A Cargo command should be executed in an integrated terminal. 101 | * `false` - A Cargo command should be executed as a child process. 102 | 103 | ### Specifying what kind of integrated terminal is used 104 | 105 | The `"rust.shell.kind.windows"` configuration parameter specifies what kind of integrated terminal is used. 106 | 107 | The configuration parameter should be specified only if the user uses Windows with [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/install_guide). 108 | 109 | In all other cases the extension should be able to determine it itself. 110 | 111 | ### Setting An Action To Handle Starting A New Command If There Is Another Command Running 112 | 113 | The `"rust.actionOnStartingCommandIfThereIsRunningCommand"` configuration parameter specifies what the extension should do in case of starting a new command if there is a previous command running. 114 | 115 | The possible values are: 116 | 117 | * `"Stop running command"` - the extension will stop the previous running command and start a new one 118 | * `"Ignore new command"` - the extension will ignore a request to start a new command 119 | * `"Show dialog to let me decide"` - the extension will show an information box to let the user decide whether or not to stop a running command 120 | 121 | ### Passing Arguments 122 | 123 | The extension supports several configuration parameters used to pass arguments on to the appropriate commands: 124 | 125 | * `"rust.buildArgs"` 126 | * `"rust.checkArgs"` 127 | * `"rust.clippyArgs"` 128 | * `"rust.docArgs"` 129 | * `"rust.runArgs"` 130 | * `"rust.testArgs"` 131 | 132 | These parameters each take an array of strings. For example, you could configure the extension to execute `cargo build --features some_feature`. 133 | 134 | These parameters are used when one of the following commands is invoked: 135 | 136 | * `"Cargo: Build"` 137 | * `"Cargo: Check"` 138 | * `"Cargo: Clippy"` 139 | * `"Cargo: Doc"` 140 | * `"Cargo: Run"` 141 | * `"Cargo: Test"` 142 | 143 | #### Examples 144 | 145 | ```json 146 | "rust.buildArgs": ["--features", "some_feature"] 147 | ``` 148 | 149 | ### Custom Configurations 150 | 151 | The extension supports several configuration parameters: 152 | 153 | * `"rust.customBuildConfigurations"` 154 | * `"rust.customCheckConfigurations"` 155 | * `"rust.customClippyConfigurations"` 156 | * `"rust.customDocConfigurations"` 157 | * `"rust.customRunConfigurations"` 158 | * `"rust.customTestConfigurations"` 159 | 160 | The type of these parameters is an array of objects and each object must have the following fields: 161 | 162 | * `"title"` - a string. It is shown as the label of a quick pick item if a Cargo command has more than one custom configuration 163 | * `"args"` - an array of strings. If a custom configuration is chosen, a Cargo command is executed with the arguments that were defined 164 | 165 | These configuration parameters are used when one of the following commands is invoked: 166 | 167 | * `"Cargo: Build using custom configuration"` 168 | * `"Cargo: Check using custom configuration"` 169 | * `"Cargo: Clippy using custom configuration"` 170 | * `"Cargo: Doc using custom configuration"` 171 | * `"Cargo: Run using custom configuration"` 172 | * `"Cargo: Test using custom configuration"` 173 | 174 | When one of these commands is invoked, the extension decides what to do: 175 | 176 | * If there are no custom configurations defined for the command, the extension shows an error message. 177 | * If only one custom configuration for the command is defined, the extension executes the customized command. 178 | * If more than one custom configuration is defined, the extension shows a quick pick view, listing the title of each configuration to let the developer decide. 179 | * If a developer cancels the quick pick, the extension does nothing. 180 | * If a developer chooses an item, the extension executes the customized command. 181 | 182 | #### Examples 183 | 184 | ##### Build Example 185 | 186 | ```json 187 | "rust.customBuildConfigurations": [ 188 | { 189 | "title": "Example: my_example", 190 | "args": ["--example", "my_example"] 191 | } 192 | ] 193 | ``` 194 | 195 | ##### Check With Features 196 | 197 | ```json 198 | "rust.customCheckConfigurations": [ 199 | { 200 | "title": "With Features", 201 | "args": ["--features", "feature1", "feature2"] 202 | } 203 | ] 204 | ``` 205 | 206 | ##### Clippy With Features 207 | 208 | ```json 209 | "rust.customClippyConfigurations": [ 210 | { 211 | "title": "With Features", 212 | "args": ["--features", "feature1", "feature2"] 213 | } 214 | ] 215 | ``` 216 | 217 | ##### Doc With Features 218 | 219 | ```json 220 | "rust.customDocConfigurations": [ 221 | { 222 | "title": "With Features", 223 | "args": ["--features", "feature1", "feature2"] 224 | } 225 | ] 226 | ``` 227 | 228 | ##### Run With Arguments 229 | 230 | ```json 231 | "rust.customRunConfigurations": [ 232 | { 233 | "title": "With Arguments", 234 | "args": ["--", "arg1", "arg2"] 235 | } 236 | ] 237 | ``` 238 | 239 | ##### Test No Run 240 | 241 | ```json 242 | "rust.customTestConfigurations": [ 243 | { 244 | "title": "No Run", 245 | "args": ["--no-run"] 246 | } 247 | ] 248 | ``` 249 | -------------------------------------------------------------------------------- /doc/changing_configuration_parameters.md: -------------------------------------------------------------------------------- 1 | # Changing Configuration Parameters Page 2 | 3 | ## How To Set Configuration Parameters 4 | 5 | Configuration parameters customize the extension's behavior. Snippets of these parameters are seen throughout the extension's documentation. As with most VS Code extensions, the file used to set these parameters is the same one used by VS Code itself: `settings.json`. The file can be accessed via Ctrl+Shift+P and typing "user settings". Once open, every configuration parameter that the extension supports will be listed on the left-hand side under "Rust extension configuration". Copy them to the right-hand side to put them into effect. 6 | 7 | ### Example Snippet 8 | ```json 9 | "rust.customRunConfigurations": [ 10 | { 11 | "title": "Release", 12 | "args": [ 13 | "--release" 14 | ] 15 | } 16 | ] 17 | ``` 18 | 19 | For more information, see the VS Code [User and Workspace Settings](https://code.visualstudio.com/docs/customization/userandworkspace) documentation. 20 | -------------------------------------------------------------------------------- /doc/common_configuration_parameters.md: -------------------------------------------------------------------------------- 1 | # Common Configuration Parameters 2 | 3 | ## rustup 4 | 5 | Users should adjust properties of this configuration parameter to customize rustup. 6 | 7 | ### toolchain 8 | 9 | This configuration parameter specifies which toolchain the extension will invoke rustup with. 10 | It is used for getting sysroot, installing components, invoking Cargo 11 | 12 | However there are few exceptions. Currently RLS is available for nightly hence RLS and rust-analysis are installed for the nightly toolchain. 13 | 14 | ### nightlyToolchain 15 | 16 | This configuration parameter specifies which toolchain the extension will invoke rustup with. 17 | It is used for installing RLS and related stuff. 18 | -------------------------------------------------------------------------------- /doc/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging Page 2 | 3 | It's possible to debug Rust code using another extension, 4 | [LLDB Debugger](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb). 5 | 6 | Install it using 7 | ``` 8 | ext install vscode-lldb 9 | ``` 10 | Make sure that LLDB is installed on your system. 11 | 12 | Afterwards set up the LLDB debugger as for an usual executable. 13 | -------------------------------------------------------------------------------- /doc/format.md: -------------------------------------------------------------------------------- 1 | # Format Page 2 | 3 | The extension supports formatting the document opened in the active text editor on saving. 4 | 5 | If the extension is runnning in [RLS Mode](rls_mode/main.md), formatting is performed via `rustfmt` integrated into RLS. 6 | 7 | If the extension is running in [Legacy Mode](legacy_mode/main.md), formatting is performed via separate `rustfmt`. 8 | 9 | Read more about [rustfmt configuration](legacy_mode/rustfmt_configuration.md) for Legacy Mode. 10 | 11 | In order to format the document, make a right click and choose `"Format Document"`. 12 | In Legacy Mode `"Format Selection"` is also available, although this feature of `rustfmt` is currently incomplete. 13 | 14 | ## Format On Save 15 | 16 | Visual Studio Code supports formatting a document on saving. 17 | 18 | Set `"editor.formatOnSave"` to `true` to make Visual Studio Code do that. 19 | -------------------------------------------------------------------------------- /doc/install_extension_from_source.md: -------------------------------------------------------------------------------- 1 | # Install Extension From Source Page 2 | 3 | Sometimes you want to have something which is already in the repository, but isn't published on the marketplace yet. 4 | 5 | There is a way to install the extension from the repository. 6 | 7 | It requires `npm` (Node Package Manager). If you don't have it you should read about installation on 8 | 9 | Furthermore I assume `npm` to be installed. 10 | 11 | Clone the source of the extension 12 | 13 | ``` 14 | git clone https://github.com/KalitaAlexey/vscode-rust 15 | ``` 16 | 17 | Or you may just download it from https://github.com/KalitaAlexey/vscode-rust 18 | 19 | Make the directory of the source be your current working directory 20 | 21 | ``` 22 | cd vscode-rust 23 | ``` 24 | 25 | Download all dependencies 26 | 27 | ``` 28 | npm install 29 | ``` 30 | 31 | You can run the extension from the source or prepare the package. 32 | 33 | ## Running From The Source 34 | 35 | Running can be used to debug the extension or to try something out without overriding the extension. 36 | 37 | In order to run the extension perform the following steps: 38 | 39 | * Open the directory of the source in Visual Studio Code 40 | * Press F5 41 | 42 | It should open a new window with the latest version of the extension. 43 | 44 | ## Creating A Package 45 | 46 | To prepare the package `vsce` is required. 47 | 48 | To install `vsce` execute: 49 | 50 | ``` 51 | npm install -g vsce 52 | ``` 53 | 54 | To prepare the package execute: 55 | 56 | ``` 57 | vsce package 58 | ``` 59 | 60 | It should create **vscode-rust-.vsix**. 61 | 62 | ## Install The Package 63 | 64 | Open Visual Studio Code. 65 | 66 | Open the "Extensions" tab in Visual Studio Code. 67 | 68 | There is "..." to the right of the "Extensions" word. 69 | 70 | Click on it. 71 | 72 | Click on "Install from VSIX...". 73 | 74 | Choose the created vsix file. 75 | 76 | Restart Visual Studio Code. 77 | 78 | Now you have the latest version. 79 | -------------------------------------------------------------------------------- /doc/legacy_mode/linting.md: -------------------------------------------------------------------------------- 1 | # Linting In Legacy Mode 2 | 3 | Executing a cargo command makes the extension parse the the command's output and show diagnostics. 4 | 5 | Legacy Mode cannot show diagnostics as you type, for that you should use [RLS Mode](../rls_mode/linting.md). 6 | 7 | Let's assume we have the following code: 8 | 9 | ```rust 10 | fn main() { 11 | let x = 5 + "10"; 12 | } 13 | ``` 14 | 15 | We then execute any cargo command. Let's execute ["Cargo: Build"](../cargo_command_execution.md). 16 | 17 | It builds and shows diagnostics: 18 | 19 | * In the source code: 20 | 21 | [![Linting](../../images/linting/code.jpg)]() 22 | 23 | * And In the Problems panel: 24 | 25 | [![Linting](../../images/linting/problems_panel_legacy_mode.jpg)]() 26 | 27 | We can hover over any diagnostic to see what it is: 28 | 29 | [![Linting](../../images/linting/code_hover_legacy_mode.jpg)]() 30 | -------------------------------------------------------------------------------- /doc/legacy_mode/main.md: -------------------------------------------------------------------------------- 1 | # Legacy Mode Main Page 2 | 3 | This page describes what **Legacy Mode** is. 4 | 5 | It is how the extension worked before the [Rust Language Server Mode](../rls_mode/main.md) had been added. 6 | 7 | ## Description 8 | 9 | The extension supports the following features for this mode: 10 | 11 | * Formatting the active document 12 | * [Executing one of the built-in Cargo commands](../cargo_command_execution.md) and showing diagnostics (warnings, errors, etc.) 13 | * Navigating to a symbol 14 | 15 | ## Required Tools 16 | 17 | Legacy Mode requires the following tools to function: 18 | 19 | * `racer` 20 | * `rustfmt` 21 | * `rustsym` 22 | 23 | If any of the tools are not found, the extension will offer to install them. A "Rust Tools Missing" item in the status bar will also appear; click on the item to install the missing tools. 24 | 25 | ## Configuration 26 | 27 | The extension supports configuration of the tools: 28 | 29 | * [Racer Configuration](racer_configuration.md) 30 | * [Rustfmt Configuration](rustfmt_configuration.md) 31 | * [Rustsym Configuration](rustsym_configuration.md) 32 | 33 | You may also configure Legacy Mode via configuration parameters. 34 | 35 | ## Configuration Parameters 36 | 37 | ### Show Output 38 | 39 | The `"rust.showOutput"` configuration parameter controls whether the output channel should be shown when [a Cargo command starts executing](../cargo_command_execution.md). 40 | 41 | The possible values: 42 | 43 | * `true` - the output channel should be shown 44 | * `false` - the output channel shouldn't be shown 45 | 46 | The output channel will not be shown under any of the following conditions: 47 | 48 | - `"rust.executeCargoCommandInTerminal"` is set to `true` 49 | - `"rust.actionOnSave"` is set to `"check"` 50 | -------------------------------------------------------------------------------- /doc/legacy_mode/racer_configuration.md: -------------------------------------------------------------------------------- 1 | # Racer Configuration Page 2 | 3 | The extension supports several configuration parameters to configure racer. 4 | 5 | ## Configuration Parameters 6 | 7 | ### Racer Path 8 | 9 | The `"rust.racerPath"` configuration parameter specifies a path to the racer's executable. 10 | 11 | The possible values: 12 | 13 | * `"Some path"` - the extension would try to use the path 14 | * `null` - the extension would try to use the `PATH` variable of the environment 15 | 16 | If the extension failed to start racer, autocompletion wouldn't be available. 17 | 18 | ### Rust Source 19 | 20 | The `"rust.rustLangSrcPath"` configuration parameter specifies a path to the `src` directory of Rust sources. 21 | 22 | The possible values: 23 | 24 | * `"Some path"` - the extension would try to use the path. If it failed the extension would try another ways to find Rust sources. 25 | * `null` - the extension would try another ways to find Rust sources. 26 | 27 | The extension tries finding Rust sources in different places: 28 | 29 | * The `"rust.rustLangSrcPath"` configuration parameter 30 | * The `RUST_SRC_PATH` variable of the environment 31 | * Rust sources installed via Rustup 32 | 33 | If the extension failed to find Rust sources, racer wouldn't provide autocompletion for the standard library. 34 | 35 | ### Cargo Home 36 | 37 | The `"rust.cargoHomePath"` configuration parameter specifies a path to the home directory of Cargo. 38 | 39 | I have never used this configuration parameter, but some people need it. 40 | -------------------------------------------------------------------------------- /doc/legacy_mode/rustfmt_configuration.md: -------------------------------------------------------------------------------- 1 | # Rustfmt Configuration Page 2 | 3 | The extension supports only one configuration parameter to configure rustfmt. 4 | 5 | The `"rust.rustfmtPath"` configuration parameter specifies a path to the rustfmt's executable. 6 | 7 | The possible values: 8 | 9 | * `"Some path"` - the extension would try to use the path 10 | * `null` - the extension would try to use the `PATH` variable of the environment 11 | 12 | If the extension failed to start rustfmt, formatting wouldn't be available. 13 | -------------------------------------------------------------------------------- /doc/legacy_mode/rustsym_configuration.md: -------------------------------------------------------------------------------- 1 | # Rustsym Configuration Page 2 | 3 | The extension supports only one configuration parameter to configure rustsym. 4 | 5 | The `"rust.rustsymPath"` configuration parameter specifies a path to the rustsym's executable. 6 | 7 | The possible values: 8 | 9 | * `"Some path"` - the extension would try to use the path 10 | * `null` - the extension would try to use the `PATH` variable of the environment 11 | 12 | If the extension failed to start rustsym, navigation to a symbol wouldn't be available. 13 | -------------------------------------------------------------------------------- /doc/linting.md: -------------------------------------------------------------------------------- 1 | # Linting Page 2 | 3 | The extension provides linting as shown in the following screenshot: 4 | 5 | [![Linting](../images/linting/code.jpg)]() 6 | 7 | It also populates the Problems panel. 8 | 9 | For the code: 10 | 11 | ```rust 12 | fn foo(i: i32) {} 13 | 14 | fn main() { 15 | foo(2i64); 16 | } 17 | ``` 18 | 19 | The Problems panel would look like: 20 | 21 | [![Linting](../images/linting/problems_panel.jpg)]() 22 | 23 | Linting behaves differently in [RLS Mode](rls_mode/linting.md) than in [Legacy Mode](legacy_mode/linting.md). 24 | -------------------------------------------------------------------------------- /doc/main.md: -------------------------------------------------------------------------------- 1 | # Main Page 2 | 3 | Welcome the main page of the documentation. 4 | 5 | First of all, issues and PRs are welcome. 6 | 7 | Each section describes various features supported by the extension and their respective configuration parameters. By [changing configuration parameters](changing_configuration_parameters.md), the extension may be customized. 8 | 9 | The extension can function in one of two modes: 10 | 11 | * [Legacy Mode](legacy_mode/main.md) 12 | * [Rust Language Server Mode](rls_mode/main.md) 13 | 14 | The first mode is called *Legacy* because this mode does its best, but the second one is better. 15 | The second one is recommended and at some point the first one will be removed. 16 | 17 | But Legacy Mode should work just fine and if it doesn't, open an issue. 18 | 19 | When the extension starts the first time, it asks to choose one of the modes. 20 | The chosen mode is stored in `"rust.mode"` and it can be changed by users. 21 | 22 | Each mode is described in detail on its own page. 23 | 24 | Some configuration parameters effect both modes. They are described [there](common_configuration_parameters.md). 25 | 26 | Furthermore, the extension provides: 27 | 28 | * [Linting (the showing of diagnostics in the active text editor)](linting.md) 29 | * [Executing one of built-in cargo command](cargo_command_execution.md) 30 | * [Creating a playground](playground_creation.md) 31 | * [Formatting a document opened in the active text editor](format.md) 32 | * [Debugging Rust programs](debugging.md) 33 | 34 | Also it provides snippets and keybindings. 35 | 36 | They are not described because you can see them on the extension's page in VSCode. 37 | 38 | ## Additional information 39 | 40 | [Install extension from source (always latest version)](install_extension_from_source.md) 41 | -------------------------------------------------------------------------------- /doc/playground_creation.md: -------------------------------------------------------------------------------- 1 | # Playground Creation Page 2 | 3 | The extension provides a way to create a project in a platform-specific temporary directory. 4 | 5 | The extension provides the command `"Cargo: Create playground"`. 6 | 7 | If a user executes the command the extension shows a quick pick to let a developer choose what kind of project to create: an application or a library. 8 | 9 | If user chooses any kind the extension performs the following steps: 10 | 11 | * Creates a directory in a platform-specific directory 12 | * Executes `cargo init ...` in the directory 13 | * Opens the directory in a new window of Visual Studio Code 14 | -------------------------------------------------------------------------------- /doc/rls_mode/linting.md: -------------------------------------------------------------------------------- 1 | # Linting in Rust Language Server Mode 2 | 3 | RLS checks the project and shows diagnostics while you are typing. 4 | 5 | You can see diagnostics in the Problems panel. 6 | 7 | You can hover over a diagnostic to see what the problem is. 8 | 9 | Executing a cargo command doesn't show any diagnostics (unlike [Legacy Mode](../legacy_mode/linting.md)). 10 | 11 | It is intentional design decision. 12 | 13 | The reason is that there is no pretty way to hide a diagnostic after the diagnostic's cause is fixed. 14 | 15 | That (the showing of a problem which has been already fixed) may confuse people, hence the decision. 16 | -------------------------------------------------------------------------------- /doc/rls_mode/main.md: -------------------------------------------------------------------------------- 1 | # Rust Language Server Mode Page 2 | 3 | The extension supports integration with [Rust Language Server]. 4 | 5 | ## Configuration 6 | 7 | There are configuration parameters which names start with `"rust.rls"`. 8 | The RLS mode can be configured by changing the parameters. 9 | 10 | ### Parameters 11 | 12 | #### rust.rls.executable 13 | 14 | Using the parameter it is possible to specify either absolute path to RLS or name of RLS. 15 | 16 | The default value is `"rls"`. 17 | 18 | It can be useful when: 19 | 20 | * There is a need in running RLS built from its source 21 | * There is a need in running RLS through some proxy (see [rust.rls.args](#rust.rls.executable])) 22 | 23 | #### rust.rls.args 24 | 25 | Using the parameter it is possible to specify what arguments to run the RLS executable with. 26 | 27 | The default value is `null`. 28 | 29 | It can be useful when: 30 | 31 | * There is a need in running RLS through some proxy, i.e., rustup 32 | 33 | #### rust.rls.env 34 | 35 | Using the parameter it is possible to specify what environment to run the RLS executable in. 36 | 37 | The default value is `null`. 38 | 39 | It can be useful when: 40 | 41 | * There is a need to pass some environment variables to the RLS executable, i.e., `RUST_LOG`, `RUST_BACKTRACE` 42 | 43 | #### rust.rls.revealOutputChannelOn 44 | 45 | Using the parameter it is possible to specify on what kind of message received from the RLS the output channel is revealed. 46 | 47 | It supports one of the following values: 48 | 49 | * `"info"` - the output channel is revealed on literally all messages 50 | * `"warn"` - the output channel is revealed on warnings 51 | * `"error"` - the output channel is revealed on errors (default) 52 | * `"never"` - the output channel is never revealed automatically (it can be revealed manually through *View->Output*) 53 | 54 | The default value is `error`. 55 | 56 | It can be useful when: 57 | 58 | * There is some problem with RLS and it sometimes sends error messages on which the output channel is revealed and it is annoying 59 | * There is a need in revealing the output channel on other kinds of message 60 | 61 | #### rust.rls.useRustfmt 62 | 63 | Using the parameter it is possible to specify if the standalone [rustfmt] is used to format code instead of the [rustfmt] embedded into RLS. 64 | 65 | It supports one of the following values: 66 | 67 | * `null` - the extension will ask if [rustfmt] is used to format code 68 | * `false` - the extension will not use [rustfmt] to format code 69 | * `true` - the extension will use [rustfmt] to format code 70 | 71 | The default value is `null`. 72 | 73 | ## Setting up 74 | 75 | The recommended way to set RLS up is using rustup. You should use rustup unless rustup does not suit you. 76 | 77 | If you can't answer if rustup suits you, then it suits you. 78 | 79 | ### Using rustup 80 | 81 | If rustup is installed on your computer, then when the extension activates it checks if RLS is installed and if it is not, then the extension asks your permission to update rustup and install RLS. 82 | 83 | If you agree with this, the extension will do it and start itself in RLS mode. 84 | 85 | You don't have to specify either settings to make RLS work because the extension will do it automatically. 86 | 87 | ### Using source 88 | 89 | First of all, you have to download the [RLS](https://github.com/rust-lang-nursery/rls) sources: 90 | 91 | ```bash 92 | git clone https://github.com/rust-lang-nursery/rls 93 | ``` 94 | 95 | Depending on whether you have rustup or not, there are different ways you can set up this plugin. 96 | 97 | * [Setting up with rustup](#with-rustup) 98 | * [Setting up without rustup](#without-rustup) 99 | 100 | #### With rustup 101 | 102 | Make sure you do have [rustup](https://github.com/rust-lang-nursery/rustup.rs) with nightly toolchain. 103 | 104 | You can use RLS either installed or by running it from the source code. 105 | 106 | If you want use RLS installed, but RLS hasn't been installed yet, perform the following steps in order to install RLS: 107 | 108 | ```bash 109 | cd /path/to/rls 110 | rustup run nightly cargo install 111 | ``` 112 | 113 | Because at the moment RLS links to the compiler and it assumes the compiler to be globally installed, one has to use rustup to start the `rls` (rustup will configure the environment accordingly): 114 | 115 | ```json 116 | "rust.rls.executable": "rustup", 117 | "rust.rls.args": ["run", "nightly", "rls"] 118 | ``` 119 | 120 | -- 121 | 122 | You can also run from source by passing `+nightly` to rustup's cargo proxy: 123 | 124 | ```json 125 | "rust.rls.executable": "cargo", 126 | "rust.rls.args": ["+nightly", "run", "--manifest-path=/path/to/rls/Cargo.toml", "--release"] 127 | ``` 128 | 129 | #### Without rustup 130 | 131 | **Note:** You should do this only if you do not have rustup because otherwise rustup will not work anymore. 132 | 133 | After you have cloned the sources, you need to download the latest nightly compiler. See the [Building section of the Rust repository](https://github.com/rust-lang/rust#building-from-source) for how to do this. 134 | 135 | You can now install the Rust Language Server globally with 136 | 137 | ```bash 138 | cd /path/to/rls 139 | cargo install 140 | ``` 141 | 142 | and set `"executable"` to `"rls"`: 143 | 144 | ```json 145 | "rust.rls.executable": "rls" 146 | ``` 147 | 148 | -- 149 | 150 | If you don't want to have it installed you can also run it from sources: 151 | 152 | ```json 153 | "rust.rls.executable": "cargo", 154 | "rust.rls.args": ["run", "--manifest-path=/path/to/rls/Cargo.toml", "--release"] 155 | ``` 156 | 157 | ## Debugging 158 | 159 | There is an output channel named "Rust Language Server" which is used to show messages from RLS. 160 | 161 | To open it, perform the following steps: 162 | 163 | * Click "View" on the menu 164 | * Click "Output" on submenu 165 | * Click on the listbox which is to the right of the shown panel 166 | * Choose "Rust Language Server" 167 | 168 | For making RLS print more data, you have to add the following lines to your [RLS] configuration: 169 | 170 | ```json 171 | "rust.rls.env": { 172 | "RUST_LOG": "rls=debug" 173 | } 174 | ``` 175 | 176 | ## Status Bar Indicator 177 | 178 | When the extension functions in RLS mode, an indicator is displayed in the status bar that shows the current status of RLS. 179 | 180 | The indicator may show one of the following statuses: 181 | 182 | * `Starting` - RLS is starting, hence no features of the extension are available 183 | * `Crashed` - RLS has crashed, hence no features of the extensions are available 184 | * `Analysis started` - RLS has begun analyzing code. Features are available, but the analysis is incomplete therefore possibly inaccurate 185 | * `Analysis finished` - RLS has finished analyzing code. Features are available and the analysis should be accurate 186 | * `Stopping` - RLS has been requested to stop. Features may or may not be available 187 | * `Stopped` - RLS has been stopped. Features are unavailable 188 | 189 | Clicking on the indicator restarts RLS. 190 | 191 | ## Enabling formatting and renaming 192 | Create a `rls.toml` file in your project's root and add `unstable_features = true` and RLS will be able to auto format on save and renaming. 193 | 194 | [rustfmt]: https://github.com/rust-lang-nursery/rustfmt 195 | [Rust Language Server]: https://github.com/rust-lang-nursery/rls 196 | [RLS]: https://github.com/rust-lang-nursery/rls 197 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var tslint = require('gulp-tslint'); 3 | var shell = require('gulp-shell'); 4 | 5 | var files = { 6 | src: 'src/**/*.ts', 7 | test: 'test/**/*.ts' 8 | }; 9 | 10 | gulp.task('compile', shell.task([ 11 | 'node ./node_modules/typescript/bin/tsc -p .' 12 | ])); 13 | 14 | gulp.task('tslint', function () { 15 | return gulp.src([files.src, files.test, '!test/index.ts']) 16 | .pipe(tslint({ 17 | formatter: 'verbose' 18 | })) 19 | .pipe(tslint.report()); 20 | }); 21 | 22 | gulp.task('default', ['compile', 'tslint']); 23 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/editor-rs/vscode-rust/f78d23430af4a6429f7b686e6c5f7c3895e2052a/images/icon.png -------------------------------------------------------------------------------- /images/linting/code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/editor-rs/vscode-rust/f78d23430af4a6429f7b686e6c5f7c3895e2052a/images/linting/code.jpg -------------------------------------------------------------------------------- /images/linting/code_hover_legacy_mode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/editor-rs/vscode-rust/f78d23430af4a6429f7b686e6c5f7c3895e2052a/images/linting/code_hover_legacy_mode.jpg -------------------------------------------------------------------------------- /images/linting/problems_panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/editor-rs/vscode-rust/f78d23430af4a6429f7b686e6c5f7c3895e2052a/images/linting/problems_panel.jpg -------------------------------------------------------------------------------- /images/linting/problems_panel_legacy_mode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/editor-rs/vscode-rust/f78d23430af4a6429f7b686e6c5f7c3895e2052a/images/linting/problems_panel_legacy_mode.jpg -------------------------------------------------------------------------------- /src/CargoInvocationManager.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from './components/configuration/Configuration'; 2 | import { Rustup } from './components/configuration/Rustup'; 3 | 4 | /** 5 | * The class defines functions which can be used to get data required to invoke Cargo 6 | */ 7 | export class CargoInvocationManager { 8 | private _rustup: Rustup | undefined; 9 | 10 | public constructor(rustup: Rustup | undefined) { 11 | this._rustup = rustup; 12 | } 13 | 14 | /** 15 | * Cargo can be accessible from multiple places, but the only one is correct. 16 | * This function determines the path to the executable which either Cargo itself or proxy to 17 | * Cargo. If the executable is a proxy to Cargo, then the proxy may require some arguments to 18 | * understand that Cargo is requested. An example is running Cargo using rustup. 19 | */ 20 | public getExecutableAndArgs(): { executable: string, args: string[] } { 21 | const userCargoPath = Configuration.getPathConfigParameter('cargoPath'); 22 | if (userCargoPath) { 23 | return { executable: userCargoPath, args: [] }; 24 | } 25 | const userToolchain = this._rustup ? this._rustup.getUserToolchain() : undefined; 26 | if (!userToolchain) { 27 | return { executable: 'cargo', args: [] }; 28 | } 29 | const args = ['run', userToolchain.toString(true, false), 'cargo']; 30 | return { executable: Rustup.getRustupExecutable(), args }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CommandLine.ts: -------------------------------------------------------------------------------- 1 | import { Shell } from './Shell'; 2 | import { correctPath } from './WslShellUtils'; 3 | 4 | /** 5 | * Creates a command to set the environment variable 6 | * @param shell The shell which the command is going to be passed to 7 | * @param varName The variable's name 8 | * @param varValue The variable's value 9 | * @return A created command which if it is passed to a terminal, 10 | * it will set the environment variable 11 | */ 12 | export function getCommandToSetEnvVar(shell: Shell, varName: string, varValue: string): string { 13 | switch (shell) { 14 | case Shell.PowerShell: 15 | return `$ENV:${varName}="${varValue}"`; 16 | case Shell.Cmd: 17 | return `set ${varName}=${varValue}`; 18 | case Shell.Shell: 19 | case Shell.Wsl: 20 | return ` export ${varName}=${varValue}`; 21 | } 22 | } 23 | 24 | /** 25 | * Escapes spaces in the specified string in the way appropriate to the specified shell 26 | * @param s The string to escape spaces in 27 | * @param shell The shell in which the string should be used 28 | * @return The string after escaping spaces 29 | */ 30 | export function escapeSpaces(s: string, shell: Shell): string { 31 | if (!s.includes(' ')) { 32 | return s; 33 | } 34 | switch (shell) { 35 | case Shell.PowerShell: 36 | // Unescape 37 | s = s.replace(new RegExp('` ', 'g'), ' '); 38 | // Escape 39 | return s.replace(new RegExp(' ', 'g'), '` '); 40 | case Shell.Cmd: 41 | s = s.concat(); 42 | if (!s.startsWith('"')) { 43 | s = '"'.concat(s); 44 | } 45 | if (!s.endsWith('"')) { 46 | s = s.concat('"'); 47 | } 48 | return s; 49 | case Shell.Shell: 50 | case Shell.Wsl: 51 | s = s.concat(); 52 | if (!s.startsWith('\'')) { 53 | s = '\''.concat(s); 54 | } 55 | if (!s.endsWith('\'')) { 56 | s = s.concat('\''); 57 | } 58 | return s; 59 | } 60 | } 61 | 62 | export function getCommandToChangeWorkingDirectory( 63 | shell: Shell, 64 | workingDirectory: string 65 | ): string { 66 | if (shell === Shell.Wsl) { 67 | workingDirectory = correctPath(workingDirectory); 68 | } 69 | return getCommandForArgs(shell, ['cd', workingDirectory]); 70 | } 71 | 72 | /** 73 | * Prepares the specified arguments to be passed to the specified shell and constructs the command 74 | * from the arguments 75 | * @param shell The shell in which the command will be executed 76 | * @param args The arguments to prepare and construct the command from 77 | * @return The command which is constructed from the specified arguments 78 | */ 79 | export function getCommandForArgs(shell: Shell, args: string[]): string { 80 | args = args.map(a => escapeSpaces(a, shell)); 81 | return args.join(' '); 82 | } 83 | 84 | /** 85 | * Creates a command to execute several statements one by one if the previous one is succeed 86 | * @param shell The shell which the command is going to be passed to 87 | * @param statements The statements to execute 88 | * @return A created command which if it is passed to a terminal, 89 | * it will execute the statements 90 | */ 91 | export function getCommandToExecuteStatementsOneByOneIfPreviousIsSucceed( 92 | shell: Shell, 93 | statements: string[] 94 | ): string { 95 | if (statements.length === 0) { 96 | return ''; 97 | } 98 | if (shell === Shell.PowerShell) { 99 | let command = statements[0]; 100 | for (let i = 1; i < statements.length; ++i) { 101 | command += `; if ($?) { ${statements[i]}; }`; 102 | } 103 | return command; 104 | } else { 105 | // The string starts with space to make sh not save the command. 106 | // This code is also executed for cmd on Windows, but leading space doesn't break anything 107 | let command = ' ' + statements[0]; 108 | for (let i = 1; i < statements.length; ++i) { 109 | command += ` && ${statements[i]}`; 110 | } 111 | return command; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/ConfigurationParameter.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceConfiguration, workspace } from 'vscode'; 2 | 3 | /** 4 | * The main goal of the class is to store the parameter's name and to expose functions to get/set 5 | * the value of the parameter 6 | */ 7 | export class ConfigurationParameter { 8 | private _sectionName: string; 9 | private _parameterName: string; 10 | 11 | public constructor(sectionName: string, parameterName: string) { 12 | this._sectionName = sectionName; 13 | this._parameterName = parameterName; 14 | } 15 | 16 | public getValue(): any { 17 | return this.getConfiguration().get(this._parameterName); 18 | } 19 | 20 | /** 21 | * Sets the value in either the user configuration or the workspace configuration 22 | * @param value The value to set 23 | * @param setToUserConfiguration The flag indicating if the value has to be set to the user settings instead 24 | * of the workspace settings 25 | */ 26 | public async setValue(value: any, setToUserConfiguration: boolean): Promise { 27 | // The configuration doesn't support `undefined`. We must convert it to `null` 28 | const convertedValue = value === undefined ? null : value; 29 | await this.getConfiguration().update( 30 | this._parameterName, 31 | convertedValue, 32 | setToUserConfiguration 33 | ); 34 | } 35 | 36 | private getConfiguration(): WorkspaceConfiguration { 37 | return workspace.getConfiguration(this._sectionName); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/IShellProvider.ts: -------------------------------------------------------------------------------- 1 | import { Shell } from './Shell'; 2 | 3 | /** 4 | * The interface declares methods which should be implemented for any shell providers 5 | */ 6 | export interface IShellProvider { 7 | getValue(): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/NotWindowsShellProvider.ts: -------------------------------------------------------------------------------- 1 | import { IShellProvider } from './IShellProvider'; 2 | import { Shell } from './Shell'; 3 | 4 | /** 5 | * The main goal of the class is to provide the current value of the shell 6 | */ 7 | export class NotWindowsShellProvider implements IShellProvider { 8 | public getValue(): Promise { 9 | // All OS expect Windows use Shell 10 | return Promise.resolve(Shell.Shell); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/OutputChannelProcess.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | import { spawn, ChildProcess, SpawnOptions } from 'child_process'; 3 | import { Readable } from 'stream'; 4 | import { OutputChannel } from 'vscode'; 5 | 6 | export interface Success { 7 | success: true; 8 | code: number; 9 | stdout: string; 10 | stderr: string; 11 | } 12 | 13 | export interface Error { 14 | success: false; 15 | } 16 | 17 | export interface Options { 18 | /** 19 | * The flag indicating whether data from stdout should be captured. By default, the data is 20 | * not captured. If the data is captured, then it will be given when the process ends 21 | */ 22 | captureStdout?: boolean; 23 | 24 | /** 25 | * The flag indicating whether data from stderr should be captured. By default, the data is 26 | * not captured. If the data is captured, then it will be given when the process ends 27 | */ 28 | captureStderr?: boolean; 29 | } 30 | 31 | export async function create(spawnCommand: string, spawnArgs: string[] | undefined, 32 | spawnOptions: SpawnOptions | undefined, outputChannelName: string): Promise { 33 | if (spawnOptions === undefined) { 34 | spawnOptions = {}; 35 | } 36 | spawnOptions.stdio = 'pipe'; 37 | const spawnedProcess = spawn(spawnCommand, spawnArgs, spawnOptions); 38 | const outputChannel = window.createOutputChannel(outputChannelName); 39 | outputChannel.show(); 40 | const result = await process(spawnedProcess, outputChannel); 41 | if (result.success && result.code === 0) { 42 | outputChannel.hide(); 43 | outputChannel.dispose(); 44 | } 45 | return result; 46 | } 47 | 48 | /** 49 | * Writes data from the process to the output channel. The function also can accept options 50 | * @param process The process to write data from. The process should be creates with 51 | * options.stdio = "pipe" 52 | * @param outputChannel The output channel to write data to 53 | * @return The result of processing the process 54 | */ 55 | export function process(process: ChildProcess, outputChannel: OutputChannel, options?: Options 56 | ): Promise { 57 | const stdout = ''; 58 | const captureStdout = getOption(options, o => o.captureStdout, false); 59 | subscribeToDataEvent(process.stdout, outputChannel, captureStdout, stdout); 60 | const stderr = ''; 61 | const captureStderr = getOption(options, o => o.captureStderr, false); 62 | subscribeToDataEvent(process.stderr, outputChannel, captureStderr, stderr); 63 | return new Promise(resolve => { 64 | const processProcessEnding = (code: number) => { 65 | resolve({ 66 | success: true, 67 | code, 68 | stdout, 69 | stderr 70 | }); 71 | }; 72 | // If some error happens, then the "error" and "close" events happen. 73 | // If the process ends, then the "exit" and "close" events happen. 74 | // It is known that the order of events is not determined. 75 | let processExited = false; 76 | let processClosed = false; 77 | process.on('error', (error: any) => { 78 | outputChannel.appendLine(`error: error=${error}`); 79 | resolve({ success: false }); 80 | }); 81 | process.on('close', (code, signal) => { 82 | outputChannel.appendLine(`\nclose: code=${code}, signal=${signal}`); 83 | processClosed = true; 84 | if (processExited) { 85 | processProcessEnding(code); 86 | } 87 | }); 88 | process.on('exit', (code, signal) => { 89 | outputChannel.appendLine(`\nexit: code=${code}, signal=${signal}`); 90 | processExited = true; 91 | if (processClosed) { 92 | processProcessEnding(code); 93 | } 94 | }); 95 | }); 96 | } 97 | 98 | function getOption(options: Options | undefined, getOption: (options: Options) => boolean | undefined, 99 | defaultValue: boolean): boolean { 100 | if (options === undefined) { 101 | return defaultValue; 102 | } 103 | const option = getOption(options); 104 | if (option === undefined) { 105 | return defaultValue; 106 | } 107 | return option; 108 | } 109 | 110 | function subscribeToDataEvent(readable: Readable, outputChannel: OutputChannel, saveData: boolean, dataStorage: string): void { 111 | readable.on('data', chunk => { 112 | const chunkAsString = typeof chunk === 'string' ? chunk : chunk.toString(); 113 | outputChannel.append(chunkAsString); 114 | if (saveData) { 115 | dataStorage += chunkAsString; 116 | } 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /src/OutputtingProcess.ts: -------------------------------------------------------------------------------- 1 | import { SpawnOptions, spawn } from 'child_process'; 2 | 3 | export interface SuccessOutput { 4 | success: true; 5 | 6 | stdoutData: string; 7 | 8 | stderrData: string; 9 | 10 | exitCode: number; 11 | } 12 | 13 | export interface FailureOutput { 14 | success: false; 15 | 16 | error: string; 17 | } 18 | 19 | export type Output = SuccessOutput | FailureOutput; 20 | 21 | /** 22 | * The class providing an ability to spawn a process and receive content of its both stdout and stderr. 23 | */ 24 | export class OutputtingProcess { 25 | /** 26 | * Spawns a new process 27 | * @param executable an executable to spawn 28 | * @param args arguments to pass to the spawned process 29 | * @param options options to use for the spawning 30 | */ 31 | public static spawn(executable: string, args?: string[], options?: SpawnOptions): Promise { 32 | const process = spawn(executable, args, options); 33 | 34 | return new Promise(resolve => { 35 | let didStdoutClose = false; 36 | 37 | let didStderrClose = false; 38 | 39 | let stdoutData = ''; 40 | 41 | let stderrData = ''; 42 | 43 | let errorOccurred = false; 44 | 45 | let didProcessClose = false; 46 | 47 | let exitCode: number | undefined = undefined; 48 | 49 | const onCloseEventOfStream = () => { 50 | if (!errorOccurred && 51 | didStderrClose && 52 | didStdoutClose && 53 | didProcessClose && 54 | exitCode !== undefined) { 55 | resolve({ success: true, stdoutData, stderrData, exitCode }); 56 | } 57 | }; 58 | 59 | const onCloseEventOfProcess = onCloseEventOfStream; 60 | 61 | const onExitEventOfProcess = onCloseEventOfProcess; 62 | 63 | process.stdout.on('data', (chunk: string | Buffer) => { 64 | if (typeof chunk === 'string') { 65 | stdoutData += chunk; 66 | } else { 67 | stdoutData += chunk.toString(); 68 | } 69 | }); 70 | 71 | process.stdout.on('close', () => { 72 | didStdoutClose = true; 73 | 74 | onCloseEventOfStream(); 75 | }); 76 | 77 | process.stderr.on('data', (chunk: string | Buffer) => { 78 | if (typeof chunk === 'string') { 79 | stderrData += chunk; 80 | } else { 81 | stderrData += chunk.toString(); 82 | } 83 | }); 84 | 85 | process.stderr.on('close', () => { 86 | didStderrClose = true; 87 | 88 | onCloseEventOfStream(); 89 | }); 90 | 91 | process.on('error', (error: any) => { 92 | errorOccurred = true; 93 | 94 | resolve({ success: false, error: error.code }); 95 | }); 96 | 97 | process.on('close', () => { 98 | didProcessClose = true; 99 | 100 | onCloseEventOfProcess(); 101 | }); 102 | 103 | process.on('exit', code => { 104 | exitCode = code; 105 | 106 | onExitEventOfProcess(); 107 | }); 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Shell.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A enumeration of possible shells 3 | */ 4 | export enum Shell { 5 | PowerShell, 6 | Cmd, 7 | Shell, 8 | Wsl 9 | } 10 | 11 | /** 12 | * The list of all shell values 13 | */ 14 | export const VALUES = [Shell.PowerShell, Shell.Cmd, Shell.Shell, Shell.Wsl]; 15 | 16 | /** 17 | * The list of textual forms of all shell values 18 | */ 19 | export const VALUE_STRINGS = VALUES.map(toString); 20 | 21 | export function fromString(s: string): Shell | undefined { 22 | switch (s) { 23 | case 'powershell': 24 | return Shell.PowerShell; 25 | case 'cmd': 26 | return Shell.Cmd; 27 | case 'shell': 28 | return Shell.Shell; 29 | case 'wsl': 30 | return Shell.Wsl; 31 | default: 32 | return undefined; 33 | } 34 | } 35 | 36 | /** 37 | * Returns the textual form of the specified shell 38 | * @param shell The shell to convert to string 39 | */ 40 | export function toString(shell: Shell): string { 41 | switch (shell) { 42 | case Shell.PowerShell: 43 | return 'powershell'; 44 | case Shell.Cmd: 45 | return 'cmd'; 46 | case Shell.Shell: 47 | return 'shell'; 48 | case Shell.Wsl: 49 | return 'wsl'; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ShellProviderManager.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from './components/logging/ILogger'; 2 | import { IShellProvider } from './IShellProvider'; 3 | import { Shell } from './Shell'; 4 | import { NotWindowsShellProvider } from './NotWindowsShellProvider'; 5 | import { WindowsShellProvider } from './WindowsShellProvider'; 6 | 7 | /** 8 | * The main goal of the class is to provide the current value of the shell 9 | */ 10 | export class ShellProviderManager { 11 | private _shellProvider: IShellProvider; 12 | 13 | /** 14 | * Creates a new object which can be used to get the current value of the shell 15 | * @param logger The logger which is used to create child logger which will be used to log 16 | * messages 17 | */ 18 | public constructor(logger: ILogger) { 19 | if (process.platform === 'win32') { 20 | this._shellProvider = new WindowsShellProvider(logger); 21 | } else { 22 | this._shellProvider = new NotWindowsShellProvider(); 23 | } 24 | } 25 | 26 | /** 27 | * Gets the current value of the shell and returns it 28 | */ 29 | public async getValue(): Promise { 30 | return await this._shellProvider.getValue(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Toolchain.ts: -------------------------------------------------------------------------------- 1 | export class Toolchain { 2 | public static readonly defaultToolchainPrefix: string = ' (default)'; 3 | 4 | /** 5 | * "stable" in "stable-x86_64-pc-windows-msvc (default)" 6 | */ 7 | public readonly channel: string; 8 | 9 | /** 10 | * "x86_64-pc-windows-msvc" in "stable-x86_64-pc-windows-msvc (default)" 11 | * `undefined` in "stable" 12 | */ 13 | public readonly host: string | undefined; 14 | 15 | /** 16 | * true in "stable-x86_64-pc-windows-msvc (default)". 17 | * false in "stable-x86_64-pc-windows-msvc" 18 | */ 19 | public readonly isDefault: boolean; 20 | 21 | /** 22 | * Tries to parse the text and if returns the toolchain parsed from the text 23 | * @param text The text to parse 24 | * @return the toolchain or undefined 25 | */ 26 | public static parse(text: string): Toolchain | undefined { 27 | const sepIndex = text.indexOf('-'); 28 | const channelEnd = sepIndex === -1 ? undefined : sepIndex; 29 | const channel = text.substring(0, channelEnd); 30 | if (channelEnd === undefined) { 31 | // The text represents the toolchain with the only channel. 32 | return new Toolchain(channel, undefined, false); 33 | } 34 | const spaceIndex = text.indexOf(' ', sepIndex); 35 | const hostEnd = spaceIndex === -1 ? undefined : spaceIndex; 36 | const host = text.substring(sepIndex + 1, hostEnd); 37 | const isDefault = text.endsWith(Toolchain.defaultToolchainPrefix); 38 | return new Toolchain(channel, host, isDefault); 39 | } 40 | 41 | public equals(toolchain: Toolchain): boolean { 42 | return this.channel === toolchain.channel && this.host === toolchain.host; 43 | } 44 | 45 | public toString(includeHost: boolean, includeIsDefault: boolean): string { 46 | let s = this.channel.concat(); 47 | if (includeHost && this.host) { 48 | s += '-'; 49 | s += this.host; 50 | } 51 | if (includeIsDefault && this.isDefault) { 52 | s += Toolchain.defaultToolchainPrefix; 53 | } 54 | return s; 55 | } 56 | 57 | private constructor(channel: string, host: string | undefined, isDefault: boolean) { 58 | this.channel = channel; 59 | this.host = host; 60 | this.isDefault = isDefault; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/UserInteraction/AskUserToAnswerYesOrNo.ts: -------------------------------------------------------------------------------- 1 | import { MessageItem, window } from 'vscode'; 2 | 3 | /** 4 | * Shows the user the dialog with the specified message and two choices: Yes or No 5 | * @param message The message to show to the user 6 | * @return The flag indicating whether the Yes choice has been chosen 7 | */ 8 | export default async function askUserToAnswerYesOrNo(message: string): Promise { 9 | const yesChoice: MessageItem = { title: 'Yes' }; 10 | const noChoice: MessageItem = { title: 'No', isCloseAffordance: true }; 11 | const choice = await window.showInformationMessage(message, { modal: true }, yesChoice, noChoice); 12 | return choice === yesChoice; 13 | } 14 | -------------------------------------------------------------------------------- /src/UserInteraction/AskUserWhatConfigurationToSaveParameterIn.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | import UserOrWorkspaceConfiguration from '../UserOrWorkspaceConfiguration'; 3 | import askUserToAnswerYesOrNo from './AskUserToAnswerYesOrNo'; 4 | 5 | /** 6 | * Shows the user the dialog to choose what configuration to save the parameter in 7 | * @return Either the choice of the user or undefined if the user dismissed the dialog 8 | */ 9 | export default async function askUserWhatConfigurationToSaveParameterIn(): Promise { 10 | const userConfigurationChoice = 'User configuration'; 11 | const workspaceConfigurationChoice = 'Workspace configuration'; 12 | while (true) { 13 | const choice = await window.showInformationMessage( 14 | 'What configuration do you want to save the parameter in?', 15 | { modal: true }, 16 | userConfigurationChoice, 17 | workspaceConfigurationChoice 18 | ); 19 | switch (choice) { 20 | case userConfigurationChoice: 21 | return UserOrWorkspaceConfiguration.User; 22 | case workspaceConfigurationChoice: 23 | return UserOrWorkspaceConfiguration.Workspace; 24 | default: 25 | // Ask the user if the dialog has been dismissed intentionally and that the 26 | // parameter shouldn't be saved. If the user doesn't confirm it, then we continue asking 27 | if (await askUserToConfirmCancellation()) { 28 | return undefined; 29 | } 30 | break; 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Asks the user if the dialog has been dismissed intentionally 37 | * @return The flag indicating if the dialog has been dismissed intentionally 38 | */ 39 | async function askUserToConfirmCancellation(): Promise { 40 | return await askUserToAnswerYesOrNo('The dialog has been dismissed. Do you want to cancel setting the configuration parameter?'); 41 | } 42 | -------------------------------------------------------------------------------- /src/UserOrWorkspaceConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The enumeration contains possible configurations supported by Visual Studio Code 3 | */ 4 | enum UserOrWorkspaceConfiguration { 5 | /** 6 | * It's also known as Global Settings, Installation-wide Settings. Properties stored in the 7 | * configuration have low precedence, but they are taken into account when the workspace 8 | * configuration doesn't define the corresponding properties 9 | */ 10 | User, 11 | /** 12 | * The configuration for the current workspace 13 | */ 14 | Workspace 15 | } 16 | 17 | // That's the only way to make an enum exported by default. 18 | // See https://github.com/Microsoft/TypeScript/issues/3792 19 | export default UserOrWorkspaceConfiguration; 20 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Surrounds the specified string by double quotes ("). 3 | * Does nothing if the string is already surrounded by them 4 | * @param s The string to surround by double quotes 5 | * @return The string surrounded by double quotes 6 | */ 7 | export function surround_by_double_quotes(s: string): string { 8 | if (!s.startsWith('"')) { 9 | s = '"'.concat(s); 10 | } 11 | if (!s.endsWith('"')) { 12 | s = s.concat('"'); 13 | } 14 | return s; 15 | } 16 | -------------------------------------------------------------------------------- /src/WindowsShellProvider.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | import { ILogger } from './components/logging/ILogger'; 3 | import askUserWhatConfigurationToSaveParameterIn from './UserInteraction/AskUserWhatConfigurationToSaveParameterIn'; 4 | import { ConfigurationParameter } from './ConfigurationParameter'; 5 | import { IShellProvider } from './IShellProvider'; 6 | import { Shell, fromString, toString, VALUE_STRINGS } from './Shell'; 7 | import UserOrWorkspaceConfiguration from './UserOrWorkspaceConfiguration'; 8 | 9 | /** 10 | * The main goal of the class is to provide the current value of the shell. 11 | * There are three sources which the class can use to determine the current value of the shell. 12 | * * From the configuration parameter `rust.shell.kind.windows` 13 | * * From the configuration parameter `terminal.integrated.shell.windows` 14 | * * Asking the user to choose any of possible shell values 15 | */ 16 | export class WindowsShellProvider implements IShellProvider { 17 | private _logger: ILogger; 18 | private _specialConfigurationParameter: ConfigurationParameter; 19 | private _gettingValueFromSpecialConfigurationParameter: GettingValueFromSpecialConfigurationParameter; 20 | private _determiningValueFromTerminalExecutable: DeterminingValueFromTerminalExecutable; 21 | private _askingUserToChooseValue: AskingUserToChooseValue; 22 | 23 | /** 24 | * Creates a new object which can be used to get the current value of the shell 25 | * @param logger The logger which is used to create child logger which will be used to log 26 | * messages 27 | */ 28 | public constructor(logger: ILogger) { 29 | this._logger = logger.createChildLogger('WindowsShellProvider: '); 30 | this._specialConfigurationParameter = new ConfigurationParameter('rust.shell.kind', 'windows'); 31 | this._gettingValueFromSpecialConfigurationParameter = new GettingValueFromSpecialConfigurationParameter(this._specialConfigurationParameter); 32 | this._determiningValueFromTerminalExecutable = new DeterminingValueFromTerminalExecutable( 33 | new ConfigurationParameter('terminal.integrated.shell', 'windows') 34 | ); 35 | this._askingUserToChooseValue = new AskingUserToChooseValue(logger); 36 | } 37 | 38 | /** 39 | * Gets the current value of the shell and returns it. This function is asynchronous because 40 | * it can ask the user to choose some value 41 | */ 42 | public async getValue(): Promise { 43 | const logger = this._logger.createChildLogger('getValue: '); 44 | const configValue = this._gettingValueFromSpecialConfigurationParameter.getValue(); 45 | if (configValue !== undefined) { 46 | logger.debug(`configValue=${configValue}`); 47 | return configValue; 48 | } 49 | const determinedValue = this._determiningValueFromTerminalExecutable.determineValue(); 50 | if (determinedValue !== undefined) { 51 | logger.debug(`determinedValue=${determinedValue}`); 52 | return determinedValue; 53 | } 54 | const userValue = await this._askingUserToChooseValue.askUser(); 55 | if (userValue !== undefined) { 56 | // The user has chosen some value. We need to save it to the special configuration 57 | // parameter to avoid asking the user in the future 58 | logger.debug(`userValue=${toString(userValue)}`); 59 | await this.trySaveUserValueToConfiguration(userValue); 60 | return userValue; 61 | } 62 | return undefined; 63 | } 64 | 65 | /** 66 | * Asks the user what configuration to save the value in and if the user chooses any, saves it 67 | * to the chosen configuration 68 | * @param userValue The value chosen by the user 69 | */ 70 | private async trySaveUserValueToConfiguration(userValue: Shell): Promise { 71 | const chosenConfiguration = await askUserWhatConfigurationToSaveParameterIn(); 72 | if (chosenConfiguration !== undefined) { 73 | this._specialConfigurationParameter.setValue(toString(userValue), chosenConfiguration === UserOrWorkspaceConfiguration.User); 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * The main goal of the class is to provide the current value of the shell from the configuration 80 | * parameter `rust.shell.kind.*` 81 | */ 82 | class GettingValueFromSpecialConfigurationParameter { 83 | private _parameter: ConfigurationParameter; 84 | 85 | /** 86 | * Creates a new object which can be used to get the current value of the shell 87 | * @param parameter The configuration parameter 88 | */ 89 | public constructor(parameter: ConfigurationParameter) { 90 | this._parameter = parameter; 91 | } 92 | 93 | /** 94 | * Gets the current value of the shell from the configuration parameter and returns it 95 | * @return if the configuration parameter contains some valid value, the value, `undefined` 96 | * otherwise 97 | */ 98 | public getValue(): Shell | undefined { 99 | const kind = this._parameter.getValue(); 100 | if (typeof kind === 'string') { 101 | return fromString(kind); 102 | } 103 | return undefined; 104 | } 105 | } 106 | 107 | /** 108 | * The main goal of the class is to provide the current value of the shell which is determined 109 | * from the configuration parameter `terminal.integrated.shell.*` 110 | */ 111 | class DeterminingValueFromTerminalExecutable { 112 | private _parameter: ConfigurationParameter; 113 | 114 | /** 115 | * Creates a new object which can be to get the current value of the shell 116 | * @param parameter The configuration parameter 117 | */ 118 | public constructor(parameter: ConfigurationParameter) { 119 | this._parameter = parameter; 120 | } 121 | 122 | /** 123 | * Determines the current value of the shell and returns it 124 | * @return if some value is determined, the value, `undefined` otherwise 125 | */ 126 | public determineValue(): Shell | undefined { 127 | const shellPath = this._parameter.getValue(); 128 | const defaultValue = undefined; 129 | if (!shellPath) { 130 | return defaultValue; 131 | } 132 | if (shellPath.includes('powershell')) { 133 | return Shell.PowerShell; 134 | } 135 | if (shellPath.includes('cmd')) { 136 | return Shell.Cmd; 137 | } 138 | return defaultValue; 139 | } 140 | } 141 | 142 | /** 143 | * The main goal of the class is to ask the user to choose some shell 144 | */ 145 | class AskingUserToChooseValue { 146 | private _logger: ILogger; 147 | 148 | /** 149 | * Creates a new object which can be used to ask the user to choose some shell 150 | * @param logger The logger to log messages 151 | */ 152 | public constructor(logger: ILogger) { 153 | this._logger = logger; 154 | } 155 | 156 | public async askUser(): Promise { 157 | const logger = this._logger.createChildLogger('askUser: '); 158 | await window.showInformationMessage('In order to run a command in the integrated terminal, the kind of shell should be chosen'); 159 | const choice = await window.showQuickPick(VALUE_STRINGS); 160 | if (!choice) { 161 | logger.debug('the user has dismissed the quick pick'); 162 | return undefined; 163 | } 164 | const shell = fromString(choice); 165 | if (shell === undefined) { 166 | logger.debug(`the user has chosen some impossible value; choice=${choice}`); 167 | return undefined; 168 | } 169 | return shell; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/WslShellUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WSL mounts disks to /mnt/. The path which can be passed to the function looks like 3 | * C:\Directory. For the path the function will return /mnt/c/Directory 4 | * @param path The path to convert 5 | */ 6 | export function correctPath(path: string): string { 7 | const disk = path.substr(0, 1).toLowerCase(); // For `C:\\Directory` it will be `C` 8 | path = path.replace(new RegExp('\\\\', 'g'), '/'); // After the line it will look like `C:/Directory` 9 | const pathWithoutDisk = path.substring(path.indexOf('/') + 1); // For `C:/Directory` it will be `Directory` 10 | return `/mnt/${disk}/${pathWithoutDisk}`; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/cargo/BuildType.ts: -------------------------------------------------------------------------------- 1 | export enum BuildType { 2 | Debug, 3 | Release 4 | } 5 | -------------------------------------------------------------------------------- /src/components/cargo/CargoManager.ts: -------------------------------------------------------------------------------- 1 | import * as tmp from 'tmp'; 2 | import { Disposable, ExtensionContext, Uri, commands, window, workspace } from 'vscode'; 3 | import { CargoInvocationManager } from '../../CargoInvocationManager'; 4 | import { ShellProviderManager } from '../../ShellProviderManager'; 5 | import { Configuration } from '../configuration/Configuration'; 6 | import { CurrentWorkingDirectoryManager } 7 | from '../configuration/current_working_directory_manager'; 8 | import { ChildLogger } from '../logging/child_logger'; 9 | import { CargoTaskManager } from './CargoTaskManager'; 10 | import { CommandInvocationReason } from './CommandInvocationReason'; 11 | import { CrateType } from './CrateType'; 12 | import { CustomConfigurationChooser } from './custom_configuration_chooser'; 13 | 14 | export class CargoManager { 15 | private _cargoTaskManager: CargoTaskManager; 16 | private _customConfigurationChooser: CustomConfigurationChooser; 17 | private _logger: ChildLogger; 18 | 19 | public constructor( 20 | context: ExtensionContext, 21 | configuration: Configuration, 22 | cargoInvocationManager: CargoInvocationManager, 23 | currentWorkingDirectoryManager: CurrentWorkingDirectoryManager, 24 | shellProviderManager: ShellProviderManager, 25 | logger: ChildLogger 26 | ) { 27 | const stopCommandName = 'rust.cargo.terminate'; 28 | this._cargoTaskManager = new CargoTaskManager( 29 | context, 30 | configuration, 31 | cargoInvocationManager, 32 | currentWorkingDirectoryManager, 33 | shellProviderManager, 34 | logger.createChildLogger('CargoTaskManager: '), 35 | stopCommandName 36 | ); 37 | this._customConfigurationChooser = new CustomConfigurationChooser(configuration); 38 | this._logger = logger; 39 | this.registerCommands(context, stopCommandName); 40 | } 41 | 42 | public executeBuildTask(reason: CommandInvocationReason): void { 43 | this._cargoTaskManager.invokeCargoBuildUsingBuildArgs(reason); 44 | } 45 | 46 | public executeCheckTask(reason: CommandInvocationReason): void { 47 | this._cargoTaskManager.invokeCargoCheckUsingCheckArgs(reason); 48 | } 49 | 50 | public executeClippyTask(reason: CommandInvocationReason): void { 51 | this._cargoTaskManager.invokeCargoClippyUsingClippyArgs(reason); 52 | } 53 | 54 | public executeDocTask(reason: CommandInvocationReason): void { 55 | this._cargoTaskManager.invokeCargoDocUsingDocArgs(reason); 56 | } 57 | 58 | public executeRunTask(reason: CommandInvocationReason): void { 59 | this._cargoTaskManager.invokeCargoRunUsingRunArgs(reason); 60 | } 61 | 62 | public executeTestTask(reason: CommandInvocationReason): void { 63 | this._cargoTaskManager.invokeCargoTestUsingTestArgs(reason); 64 | } 65 | 66 | public registerCommandHelpingCreatePlayground(commandName: string): Disposable { 67 | return commands.registerCommand(commandName, () => { 68 | this.helpCreatePlayground(); 69 | }); 70 | } 71 | 72 | public registerCommandHelpingChooseArgsAndInvokingCargoCheck(commandName: string): Disposable { 73 | return commands.registerCommand(commandName, () => { 74 | this._customConfigurationChooser.choose('customCheckConfigurations').then(args => { 75 | this._cargoTaskManager.invokeCargoCheckWithArgs(args, CommandInvocationReason.CommandExecution); 76 | }, () => undefined); 77 | }); 78 | } 79 | 80 | public registerCommandInvokingCargoCheckUsingCheckArgs(commandName: string): Disposable { 81 | return commands.registerCommand(commandName, () => { 82 | this.executeCheckTask(CommandInvocationReason.CommandExecution); 83 | }); 84 | } 85 | 86 | public registerCommandHelpingChooseArgsAndInvokingCargoClippy(commandName: string): Disposable { 87 | return commands.registerCommand(commandName, () => { 88 | this._customConfigurationChooser.choose('customClippyConfigurations').then(args => { 89 | this._cargoTaskManager.invokeCargoClippyWithArgs(args, CommandInvocationReason.CommandExecution); 90 | }, () => undefined); 91 | }); 92 | } 93 | 94 | public registerCommandInvokingCargoClippyUsingClippyArgs(commandName: string): Disposable { 95 | return commands.registerCommand(commandName, () => { 96 | this.executeClippyTask(CommandInvocationReason.CommandExecution); 97 | }); 98 | } 99 | 100 | public registerCommandHelpingChooseArgsAndInvokingCargoDoc(commandName: string): Disposable { 101 | return commands.registerCommand(commandName, () => { 102 | this._customConfigurationChooser.choose('customDocConfigurations').then(args => { 103 | this._cargoTaskManager.invokeCargoDocWithArgs(args, CommandInvocationReason.CommandExecution); 104 | }, () => undefined); 105 | }); 106 | } 107 | 108 | public registerCommandInvokingCargoDocUsingDocArgs(commandName: string): Disposable { 109 | return commands.registerCommand(commandName, () => { 110 | this.executeDocTask(CommandInvocationReason.CommandExecution); 111 | }); 112 | } 113 | 114 | public registerCommandHelpingCreateProject(commandName: string, isBin: boolean): Disposable { 115 | return commands.registerCommand(commandName, () => { 116 | const cwd = workspace.rootPath; 117 | if (!cwd) { 118 | window.showErrorMessage('Current document not in the workspace'); 119 | return; 120 | } 121 | const projectType = isBin ? 'executable' : 'library'; 122 | const placeHolder = `Enter ${projectType} project name`; 123 | window.showInputBox({ placeHolder: placeHolder }).then((name: string) => { 124 | if (!name || name.length === 0) { 125 | return; 126 | } 127 | this._cargoTaskManager.invokeCargoNew(name, isBin, cwd); 128 | }); 129 | }); 130 | } 131 | 132 | public registerCommandHelpingChooseArgsAndInvokingCargoBuild(commandName: string): Disposable { 133 | return commands.registerCommand(commandName, () => { 134 | this._customConfigurationChooser.choose('customBuildConfigurations').then(args => { 135 | this._cargoTaskManager.invokeCargoBuildWithArgs(args, CommandInvocationReason.CommandExecution); 136 | }, () => undefined); 137 | }); 138 | } 139 | 140 | public registerCommandInvokingCargoBuildUsingBuildArgs(commandName: string): Disposable { 141 | return commands.registerCommand(commandName, () => { 142 | this.executeBuildTask(CommandInvocationReason.CommandExecution); 143 | }); 144 | } 145 | 146 | public registerCommandHelpingChooseArgsAndInvokingCargoRun(commandName: string): Disposable { 147 | return commands.registerCommand(commandName, () => { 148 | this._customConfigurationChooser.choose('customRunConfigurations').then(args => { 149 | this._cargoTaskManager.invokeCargoRunWithArgs(args, CommandInvocationReason.CommandExecution); 150 | }, () => undefined); 151 | }); 152 | } 153 | 154 | public registerCommandInvokingCargoRunUsingRunArgs(commandName: string): Disposable { 155 | return commands.registerCommand(commandName, () => { 156 | this.executeRunTask(CommandInvocationReason.CommandExecution); 157 | }); 158 | } 159 | 160 | public registerCommandHelpingChooseArgsAndInvokingCargoTest(commandName: string): Disposable { 161 | return commands.registerCommand(commandName, () => { 162 | this._customConfigurationChooser.choose('customTestConfigurations').then(args => { 163 | this._cargoTaskManager.invokeCargoTestWithArgs(args, CommandInvocationReason.CommandExecution); 164 | }, () => undefined); 165 | }); 166 | } 167 | 168 | public registerCommandInvokingCargoTestUsingTestArgs(commandName: string): Disposable { 169 | return commands.registerCommand(commandName, () => { 170 | this.executeTestTask(CommandInvocationReason.CommandExecution); 171 | }); 172 | } 173 | 174 | public registerCommandInvokingCargoWithArgs(commandName: string, command: string, ...args: string[]): Disposable { 175 | return commands.registerCommand(commandName, () => { 176 | this._cargoTaskManager.invokeCargo(command, args); 177 | }); 178 | } 179 | 180 | public registerCommandStoppingCargoTask(commandName: string): Disposable { 181 | return commands.registerCommand(commandName, () => { 182 | this._cargoTaskManager.stopTask(); 183 | }); 184 | } 185 | 186 | private registerCommands(context: ExtensionContext, stopCommandName: string): void { 187 | // Cargo init 188 | context.subscriptions.push(this.registerCommandHelpingCreatePlayground('rust.cargo.new.playground')); 189 | // Cargo new 190 | context.subscriptions.push(this.registerCommandHelpingCreateProject('rust.cargo.new.bin', true)); 191 | context.subscriptions.push(this.registerCommandHelpingCreateProject('rust.cargo.new.lib', false)); 192 | // Cargo build 193 | context.subscriptions.push(this.registerCommandInvokingCargoBuildUsingBuildArgs('rust.cargo.build.default')); 194 | context.subscriptions.push(this.registerCommandHelpingChooseArgsAndInvokingCargoBuild('rust.cargo.build.custom')); 195 | // Cargo run 196 | context.subscriptions.push(this.registerCommandInvokingCargoRunUsingRunArgs('rust.cargo.run.default')); 197 | context.subscriptions.push(this.registerCommandHelpingChooseArgsAndInvokingCargoRun('rust.cargo.run.custom')); 198 | // Cargo test 199 | context.subscriptions.push(this.registerCommandInvokingCargoTestUsingTestArgs('rust.cargo.test.default')); 200 | context.subscriptions.push(this.registerCommandHelpingChooseArgsAndInvokingCargoTest('rust.cargo.test.custom')); 201 | // Cargo bench 202 | context.subscriptions.push(this.registerCommandInvokingCargoWithArgs('rust.cargo.bench', 'bench')); 203 | // Cargo doc 204 | context.subscriptions.push(this.registerCommandInvokingCargoDocUsingDocArgs('rust.cargo.doc.default')); 205 | context.subscriptions.push(this.registerCommandHelpingChooseArgsAndInvokingCargoDoc('rust.cargo.doc.custom')); 206 | // Cargo update 207 | context.subscriptions.push(this.registerCommandInvokingCargoWithArgs('rust.cargo.update', 'update')); 208 | // Cargo clean 209 | context.subscriptions.push(this.registerCommandInvokingCargoWithArgs('rust.cargo.clean', 'clean')); 210 | // Cargo check 211 | context.subscriptions.push(this.registerCommandInvokingCargoCheckUsingCheckArgs('rust.cargo.check.default')); 212 | context.subscriptions.push(this.registerCommandHelpingChooseArgsAndInvokingCargoCheck('rust.cargo.check.custom')); 213 | // Cargo clippy 214 | context.subscriptions.push(this.registerCommandInvokingCargoClippyUsingClippyArgs('rust.cargo.clippy.default')); 215 | context.subscriptions.push(this.registerCommandHelpingChooseArgsAndInvokingCargoClippy('rust.cargo.clippy.custom')); 216 | // Cargo terminate 217 | context.subscriptions.push(this.registerCommandStoppingCargoTask(stopCommandName)); 218 | } 219 | 220 | private helpCreatePlayground(): void { 221 | const logger = this._logger.createChildLogger('helpCreatePlayground: '); 222 | const playgroundProjectTypes = ['application', 'library']; 223 | window.showQuickPick(playgroundProjectTypes) 224 | .then((playgroundProjectType: string | undefined) => { 225 | if (!playgroundProjectType) { 226 | logger.debug('quick pick has been cancelled'); 227 | return; 228 | } 229 | tmp.dir((err, path) => { 230 | if (err) { 231 | this._logger.error(`Temporary directory creation failed: ${err}`); 232 | window.showErrorMessage('Temporary directory creation failed'); 233 | return; 234 | } 235 | const crateType = playgroundProjectType === 'application' ? CrateType.Application : CrateType.Library; 236 | const name = `playground_${playgroundProjectType}`; 237 | this._cargoTaskManager.invokeCargoInit(crateType, name, path) 238 | .then(() => { 239 | const uri = Uri.parse(path); 240 | 241 | commands.executeCommand('vscode.openFolder', uri, true); 242 | }); 243 | }); 244 | }); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/components/cargo/CheckTarget.ts: -------------------------------------------------------------------------------- 1 | export enum CheckTarget { 2 | Library, 3 | Application 4 | } 5 | -------------------------------------------------------------------------------- /src/components/cargo/CommandInvocationReason.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Possible reasons of a cargo command invocation 3 | */ 4 | export enum CommandInvocationReason { 5 | /** 6 | * The command is invoked because the action on save is to execute the command 7 | */ 8 | ActionOnSave, 9 | /** 10 | * The command is invoked because the corresponding registered command is executed 11 | */ 12 | CommandExecution 13 | } 14 | -------------------------------------------------------------------------------- /src/components/cargo/CrateType.ts: -------------------------------------------------------------------------------- 1 | export enum CrateType { 2 | Application, 3 | Library 4 | } 5 | -------------------------------------------------------------------------------- /src/components/cargo/UserDefinedArgs.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from '../configuration/Configuration'; 2 | 3 | export class UserDefinedArgs { 4 | public static getBuildArgs(): string[] { 5 | return UserDefinedArgs.getArgs('buildArgs'); 6 | } 7 | 8 | public static getCheckArgs(): string[] { 9 | return UserDefinedArgs.getArgs('checkArgs'); 10 | } 11 | 12 | public static getClippyArgs(): string[] { 13 | return UserDefinedArgs.getArgs('clippyArgs'); 14 | } 15 | 16 | public static getDocArgs(): string[] { 17 | return UserDefinedArgs.getArgs('docArgs'); 18 | } 19 | 20 | public static getRunArgs(): string[] { 21 | return UserDefinedArgs.getArgs('runArgs'); 22 | } 23 | 24 | public static getTestArgs(): string[] { 25 | return UserDefinedArgs.getArgs('testArgs'); 26 | } 27 | 28 | private static getArgs(property: string): string[] { 29 | const configuration = Configuration.getConfiguration(); 30 | return configuration.get(property, []); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/cargo/custom_configuration_chooser.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem, window } from 'vscode'; 2 | 3 | import { Configuration } from '../configuration/Configuration'; 4 | 5 | interface CustomConfiguration { 6 | title: string; 7 | 8 | args: string[]; 9 | } 10 | 11 | class CustomConfigurationQuickPickItem implements QuickPickItem { 12 | public label: string; 13 | 14 | public description: string; 15 | 16 | public args: string[]; 17 | 18 | public constructor(cfg: CustomConfiguration) { 19 | this.label = cfg.title; 20 | 21 | this.description = ''; 22 | 23 | this.args = cfg.args; 24 | } 25 | } 26 | 27 | export class CustomConfigurationChooser { 28 | private configuration: Configuration; 29 | 30 | public constructor(configuration: Configuration) { 31 | this.configuration = configuration; 32 | } 33 | 34 | public choose(propertyName: string): Thenable { 35 | const configuration = Configuration.getConfiguration(); 36 | 37 | const customConfigurations = configuration.get(propertyName); 38 | 39 | if (!customConfigurations) { 40 | throw new Error(`No custom configurations for property=${propertyName}`); 41 | } 42 | 43 | if (customConfigurations.length === 0) { 44 | window.showErrorMessage('There are no defined custom configurations'); 45 | 46 | return Promise.reject(null); 47 | } 48 | 49 | if (customConfigurations.length === 1) { 50 | const customConfiguration = customConfigurations[0]; 51 | 52 | const args = customConfiguration.args; 53 | 54 | return Promise.resolve(args); 55 | } 56 | 57 | const quickPickItems = customConfigurations.map(c => new CustomConfigurationQuickPickItem(c)); 58 | 59 | return window.showQuickPick(quickPickItems).then(item => { 60 | if (!item) { 61 | return Promise.reject(null); 62 | } 63 | 64 | return Promise.resolve(item.args); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/cargo/diagnostic_parser.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic, DiagnosticSeverity, Range } from 'vscode'; 2 | 3 | import { FileDiagnostic } from './file_diagnostic'; 4 | 5 | interface CompilerMessageSpanText { 6 | highlight_end: number; 7 | highlight_start: number; 8 | text: string; 9 | } 10 | 11 | interface CompilerMessageCode { 12 | code: string; 13 | explanation: string; 14 | } 15 | 16 | interface CompilerMessageSpanExpansion { 17 | def_site_span: CompilerMessageSpan; 18 | macro_decl_name: string; 19 | span: CompilerMessageSpan; 20 | } 21 | 22 | interface CompilerMessageSpan { 23 | byte_end: number; 24 | byte_start: number; 25 | column_end: number; 26 | column_start: number; 27 | expansion?: CompilerMessageSpanExpansion; 28 | file_name: string; 29 | is_primary: boolean; 30 | label: string; 31 | line_end: number; 32 | line_start: number; 33 | suggested_replacement?: any; // I don't know what type it has 34 | text: CompilerMessageSpanText[]; 35 | } 36 | 37 | interface CompilerMessage { 38 | children: any[]; // I don't know what type it has 39 | code?: CompilerMessageCode; 40 | level: string; 41 | message: string; 42 | rendered?: any; // I don't know what type it has 43 | spans: CompilerMessageSpan[]; 44 | } 45 | 46 | interface CargoMessageTarget { 47 | kind: string[]; 48 | name: string; 49 | src_path: string; 50 | } 51 | 52 | interface CargoMessageWithCompilerMessage { 53 | message: CompilerMessage; 54 | package_id: string; 55 | reason: 'compiler-message'; 56 | target: CargoMessageTarget; 57 | } 58 | 59 | interface CargoMessageWithCompilerArtifact { 60 | features: any[]; 61 | filenames: string[]; 62 | package_id: string; 63 | profile: any; 64 | reason: 'compiler-artifact'; 65 | target: CargoMessageTarget; 66 | } 67 | 68 | export class DiagnosticParser { 69 | /** 70 | * Parses diagnostics from a line 71 | * @param line A line to parse 72 | * @return parsed diagnostics 73 | */ 74 | public parseLine(line: string): FileDiagnostic[] { 75 | const cargoMessage: CargoMessageWithCompilerArtifact | CargoMessageWithCompilerMessage = 76 | JSON.parse(line); 77 | 78 | if (cargoMessage.reason === 'compiler-message') { 79 | return this.parseCompilerMessage(cargoMessage.message); 80 | } else { 81 | return []; 82 | } 83 | } 84 | 85 | private parseCompilerMessage(compilerMessage: CompilerMessage): FileDiagnostic[] { 86 | const spans = compilerMessage.spans; 87 | 88 | if (spans.length === 0) { 89 | return []; 90 | } 91 | 92 | // Only add the primary span, as VSCode orders the problem window by the 93 | // error's range, which causes a lot of confusion if there are duplicate messages. 94 | let primarySpan = spans.find(span => span.is_primary); 95 | 96 | if (!primarySpan) { 97 | return []; 98 | } 99 | 100 | // Following macro expansion to get correct file name and range. 101 | while (primarySpan.expansion && primarySpan.expansion.span && primarySpan.expansion.macro_decl_name !== 'include!') { 102 | primarySpan = primarySpan.expansion.span; 103 | } 104 | 105 | const range = new Range( 106 | primarySpan.line_start - 1, 107 | primarySpan.column_start - 1, 108 | primarySpan.line_end - 1, 109 | primarySpan.column_end - 1 110 | ); 111 | 112 | let message = compilerMessage.message; 113 | 114 | if (compilerMessage.code) { 115 | message = `${compilerMessage.code.code}: ${message}`; 116 | } 117 | 118 | if (primarySpan.label) { 119 | message += `\n label: ${primarySpan.label}`; 120 | } 121 | 122 | message = this.addNotesToMessage(message, compilerMessage.children, 1); 123 | 124 | const diagnostic = new Diagnostic(range, message, this.toSeverity(compilerMessage.level)); 125 | 126 | const fileDiagnostic = { filePath: primarySpan.file_name, diagnostic: diagnostic }; 127 | 128 | return [fileDiagnostic]; 129 | } 130 | 131 | private toSeverity(severity: string): DiagnosticSeverity { 132 | switch (severity) { 133 | case 'warning': 134 | return DiagnosticSeverity.Warning; 135 | 136 | case 'note': 137 | return DiagnosticSeverity.Information; 138 | 139 | case 'help': 140 | return DiagnosticSeverity.Hint; 141 | 142 | default: 143 | return DiagnosticSeverity.Error; 144 | } 145 | } 146 | 147 | private addNotesToMessage(msg: string, children: any[], level: number): string { 148 | const indentation = ' '.repeat(level); 149 | 150 | for (const child of children) { 151 | msg += `\n${indentation}${child.level}: ${child.message}`; 152 | 153 | if (child.spans && child.spans.length > 0) { 154 | msg += ': '; 155 | const lines = []; 156 | 157 | for (const span of child.spans) { 158 | if (!span.file_name || !span.line_start) { 159 | continue; 160 | } 161 | 162 | lines.push(`${span.file_name}(${span.line_start})`); 163 | } 164 | 165 | msg += lines.join(', '); 166 | } 167 | 168 | if (child.children) { 169 | msg = this.addNotesToMessage(msg, child.children, level + 1); 170 | } 171 | } 172 | 173 | return msg; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/components/cargo/diagnostic_utils.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute, join } from 'path'; 2 | 3 | import { Diagnostic, DiagnosticCollection, Uri } from 'vscode'; 4 | 5 | import { FileDiagnostic } from './file_diagnostic'; 6 | 7 | /** 8 | * The path of a diagnostic must be absolute. 9 | * The function prepends the path of the project to the path of the diagnostic. 10 | * @param diagnosticPath The path of the diagnostic 11 | * @param projectPath The path of the project 12 | */ 13 | export function normalizeDiagnosticPath(diagnosticPath: string, projectPath: string): string { 14 | if (isAbsolute(diagnosticPath)) { 15 | return diagnosticPath; 16 | } else { 17 | return join(projectPath, diagnosticPath); 18 | } 19 | } 20 | 21 | /** 22 | * Adds the diagnostic to the diagnostics only if the diagnostic isn't in the diagnostics. 23 | * @param diagnostic The diagnostic to add 24 | * @param diagnostics The collection of diagnostics to take the diagnostic 25 | */ 26 | export function addUniqueDiagnostic(diagnostic: FileDiagnostic, diagnostics: DiagnosticCollection): void { 27 | const uri = Uri.file(diagnostic.filePath); 28 | 29 | const fileDiagnostics = diagnostics.get(uri); 30 | 31 | if (!fileDiagnostics) { 32 | // No diagnostics for the file 33 | // The diagnostic is unique 34 | diagnostics.set(uri, [diagnostic.diagnostic]); 35 | } else if (isUniqueDiagnostic(diagnostic.diagnostic, fileDiagnostics)) { 36 | const newFileDiagnostics = fileDiagnostics.concat([diagnostic.diagnostic]); 37 | diagnostics.set(uri, newFileDiagnostics); 38 | } 39 | } 40 | 41 | export function isUniqueDiagnostic(diagnostic: Diagnostic, diagnostics: Diagnostic[]): boolean { 42 | const foundDiagnostic = diagnostics.find(uniqueDiagnostic => { 43 | if (!diagnostic.range.isEqual(uniqueDiagnostic.range)) { 44 | return false; 45 | } 46 | 47 | if (diagnostic.message !== uniqueDiagnostic.message) { 48 | return false; 49 | } 50 | 51 | return true; 52 | }); 53 | 54 | return !foundDiagnostic; 55 | } 56 | -------------------------------------------------------------------------------- /src/components/cargo/file_diagnostic.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic } from 'vscode'; 2 | 3 | export interface FileDiagnostic { 4 | filePath: string; 5 | 6 | diagnostic: Diagnostic; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/cargo/helper.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | 3 | import { 4 | ActionOnStartingCommandIfThereIsRunningCommand, 5 | Configuration 6 | } from '../configuration/Configuration'; 7 | 8 | export enum CommandStartHandleResult { 9 | StopRunningCommand, 10 | IgnoreNewCommand 11 | } 12 | 13 | /** 14 | * The class stores functionality which can't be placed somewhere else. 15 | */ 16 | export class Helper { 17 | private configuration: Configuration; 18 | 19 | public constructor(configuration: Configuration) { 20 | this.configuration = configuration; 21 | } 22 | 23 | public handleCommandStartWhenThereIsRunningCommand(): Promise { 24 | const action = 25 | this.configuration.getActionOnStartingCommandIfThereIsRunningCommand(); 26 | 27 | switch (action) { 28 | case ActionOnStartingCommandIfThereIsRunningCommand.ShowDialogToLetUserDecide: 29 | return new Promise(async (resolve) => { 30 | const choice = await window.showInformationMessage( 31 | 'You requested to start a command, but there is another running command', 32 | 'Terminate' 33 | ); 34 | 35 | if (choice === 'Terminate') { 36 | resolve(CommandStartHandleResult.StopRunningCommand); 37 | } else { 38 | resolve(CommandStartHandleResult.IgnoreNewCommand); 39 | } 40 | }); 41 | 42 | case ActionOnStartingCommandIfThereIsRunningCommand.StopRunningCommand: 43 | return Promise.resolve(CommandStartHandleResult.StopRunningCommand); 44 | 45 | case ActionOnStartingCommandIfThereIsRunningCommand.IgnoreNewCommand: 46 | return Promise.resolve(CommandStartHandleResult.IgnoreNewCommand); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/cargo/output_channel_task_manager.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticCollection, languages, window } from 'vscode'; 2 | import { Configuration } from '../configuration/Configuration'; 3 | import { ChildLogger } from '../logging/child_logger'; 4 | import { DiagnosticParser } from './diagnostic_parser'; 5 | import { normalizeDiagnosticPath, addUniqueDiagnostic } from './diagnostic_utils'; 6 | import { OutputChannelWrapper } from './output_channel_wrapper'; 7 | import { OutputChannelTaskStatusBarItem } from './output_channel_task_status_bar_item'; 8 | import { ExitCode, Task } from './task'; 9 | 10 | export class OutputChannelTaskManager { 11 | private channel: OutputChannelWrapper; 12 | private configuration: Configuration; 13 | private logger: ChildLogger; 14 | private runningTask: Task | undefined; 15 | private diagnostics: DiagnosticCollection; 16 | private diagnosticParser: DiagnosticParser; 17 | private statusBarItem: OutputChannelTaskStatusBarItem; 18 | 19 | public constructor( 20 | configuration: Configuration, 21 | logger: ChildLogger, 22 | stopCommandName: string 23 | ) { 24 | this.channel = new OutputChannelWrapper(window.createOutputChannel('Cargo')); 25 | this.configuration = configuration; 26 | this.logger = logger; 27 | this.diagnostics = languages.createDiagnosticCollection('rust'); 28 | this.diagnosticParser = new DiagnosticParser(); 29 | this.statusBarItem = new OutputChannelTaskStatusBarItem(stopCommandName); 30 | } 31 | 32 | public async startTask( 33 | executable: string, 34 | preCommandArgs: string[], 35 | command: string, 36 | args: string[], 37 | cwd: string, 38 | parseOutput: boolean, 39 | shouldShowOutputChannnel: boolean 40 | ): Promise { 41 | function prependArgsWithMessageFormatIfRequired(): void { 42 | if (!parseOutput) { 43 | return; 44 | } 45 | 46 | // Prepend arguments with arguments making cargo print output in JSON. 47 | switch (command) { 48 | case 'build': 49 | case 'check': 50 | case 'clippy': 51 | case 'test': 52 | case 'run': 53 | args = ['--message-format', 'json'].concat(args); 54 | break; 55 | } 56 | } 57 | prependArgsWithMessageFormatIfRequired(); 58 | args = preCommandArgs.concat(command, ...args); 59 | this.runningTask = new Task( 60 | this.configuration, 61 | this.logger.createChildLogger('Task: '), 62 | executable, 63 | args, 64 | cwd 65 | ); 66 | this.runningTask.setStarted(() => { 67 | this.channel.clear(); 68 | this.channel.append(`Working directory: ${cwd}\n`); 69 | this.channel.append(`Started ${executable} ${args.join(' ')}\n\n`); 70 | this.diagnostics.clear(); 71 | }); 72 | this.runningTask.setLineReceivedInStdout(line => { 73 | if (parseOutput && line.startsWith('{')) { 74 | const fileDiagnostics = this.diagnosticParser.parseLine(line); 75 | for (const fileDiagnostic of fileDiagnostics) { 76 | fileDiagnostic.filePath = normalizeDiagnosticPath(fileDiagnostic.filePath, cwd); 77 | addUniqueDiagnostic(fileDiagnostic, this.diagnostics); 78 | } 79 | } else { 80 | this.channel.append(`${line}\n`); 81 | } 82 | }); 83 | this.runningTask.setLineReceivedInStderr(line => { 84 | this.channel.append(`${line}\n`); 85 | }); 86 | if (shouldShowOutputChannnel) { 87 | this.channel.show(); 88 | } 89 | this.statusBarItem.show(); 90 | let exitCode: ExitCode; 91 | try { 92 | exitCode = await this.runningTask.execute(); 93 | } catch (error) { 94 | this.statusBarItem.hide(); 95 | this.runningTask = undefined; 96 | // No error means the task has been interrupted 97 | if (error && error.message === 'ENOENT') { 98 | const message = 'The "cargo" command is not available. Make sure it is installed.'; 99 | window.showInformationMessage(message); 100 | } 101 | return; 102 | } 103 | this.statusBarItem.hide(); 104 | this.runningTask = undefined; 105 | this.channel.append(`\nCompleted with code ${exitCode}\n`); 106 | } 107 | 108 | public hasRunningTask(): boolean { 109 | return this.runningTask !== undefined; 110 | } 111 | 112 | public async stopRunningTask(): Promise { 113 | if (this.runningTask !== undefined) { 114 | await this.runningTask.kill(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/cargo/output_channel_task_status_bar_item.ts: -------------------------------------------------------------------------------- 1 | import elegantSpinner = require('elegant-spinner'); 2 | 3 | import { StatusBarItem, window } from 'vscode'; 4 | 5 | export class OutputChannelTaskStatusBarItem { 6 | private stopStatusBarItem: StatusBarItem; 7 | 8 | private spinnerStatusBarItem: StatusBarItem; 9 | 10 | private interval: NodeJS.Timer | undefined; 11 | 12 | public constructor(stopCommandName: string) { 13 | this.stopStatusBarItem = window.createStatusBarItem(); 14 | this.stopStatusBarItem.command = stopCommandName; 15 | this.stopStatusBarItem.text = 'Stop'; 16 | this.stopStatusBarItem.tooltip = 'Click to stop running cargo task'; 17 | 18 | this.spinnerStatusBarItem = window.createStatusBarItem(); 19 | this.spinnerStatusBarItem.tooltip = 'Cargo task is running'; 20 | } 21 | 22 | public show(): void { 23 | this.stopStatusBarItem.show(); 24 | 25 | this.spinnerStatusBarItem.show(); 26 | 27 | const spinner = elegantSpinner(); 28 | 29 | const update = () => { 30 | this.spinnerStatusBarItem.text = spinner(); 31 | }; 32 | 33 | this.interval = setInterval(update, 100); 34 | } 35 | 36 | public hide(): void { 37 | if (this.interval !== undefined) { 38 | clearInterval(this.interval); 39 | 40 | this.interval = undefined; 41 | } 42 | 43 | this.stopStatusBarItem.hide(); 44 | 45 | this.spinnerStatusBarItem.hide(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/cargo/output_channel_wrapper.ts: -------------------------------------------------------------------------------- 1 | import { OutputChannel } from 'vscode'; 2 | 3 | export class OutputChannelWrapper { 4 | private channel: OutputChannel; 5 | 6 | public constructor(channel: OutputChannel) { 7 | this.channel = channel; 8 | } 9 | 10 | public append(message: string): void { 11 | this.channel.append(message); 12 | } 13 | 14 | public clear(): void { 15 | this.channel.clear(); 16 | } 17 | 18 | public show(): void { 19 | this.channel.show(true); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/cargo/task.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn as spawn_process } from 'child_process'; 2 | import kill = require('tree-kill'); 3 | import * as readline from 'readline'; 4 | import { Configuration } from '../configuration/Configuration'; 5 | import { ChildLogger } from '../logging/child_logger'; 6 | 7 | export type ExitCode = number; 8 | 9 | export class Task { 10 | private configuration: Configuration; 11 | private logger: ChildLogger; 12 | private executable: string; 13 | private args: string[]; 14 | private cwd: string; 15 | private onStarted?: () => void; 16 | private onLineReceivedInStderr?: (line: string) => void; 17 | private onLineReceivedInStdout?: (line: string) => void; 18 | private process: ChildProcess | undefined; 19 | private interrupted: boolean; 20 | 21 | public constructor( 22 | configuration: Configuration, 23 | logger: ChildLogger, 24 | executable: string, 25 | args: string[], 26 | cwd: string 27 | ) { 28 | this.configuration = configuration; 29 | this.logger = logger; 30 | this.executable = executable; 31 | this.args = args; 32 | this.cwd = cwd; 33 | this.onStarted = undefined; 34 | this.onLineReceivedInStderr = undefined; 35 | this.onLineReceivedInStdout = undefined; 36 | this.process = undefined; 37 | this.interrupted = false; 38 | } 39 | 40 | public setStarted(onStarted: () => void): void { 41 | this.onStarted = onStarted; 42 | } 43 | 44 | public setLineReceivedInStderr(onLineReceivedInStderr: (line: string) => void): void { 45 | this.onLineReceivedInStderr = onLineReceivedInStderr; 46 | } 47 | 48 | public setLineReceivedInStdout(onLineReceivedInStdout: (line: string) => void): void { 49 | this.onLineReceivedInStdout = onLineReceivedInStdout; 50 | } 51 | 52 | public execute(): Thenable { 53 | return new Promise((resolve, reject) => { 54 | let env = Object.assign({}, process.env); 55 | const cargoEnv = this.configuration.getCargoEnv(); 56 | if (cargoEnv) { 57 | env = Object.assign(env, cargoEnv); 58 | } 59 | this.logger.debug(`execute: this.executable = "${this.executable}"`); 60 | this.logger.debug(`execute: this.args = ${JSON.stringify(this.args)}`); 61 | this.logger.debug(`execute: cargoEnv = ${JSON.stringify(cargoEnv)}`); 62 | if (this.onStarted) { 63 | this.onStarted(); 64 | } 65 | const spawnedProcess: ChildProcess = spawn_process(this.executable, this.args, { cwd: this.cwd, env }); 66 | this.process = spawnedProcess; 67 | if (this.onLineReceivedInStdout !== undefined) { 68 | const onLineReceivedInStdout = this.onLineReceivedInStdout; 69 | const stdout = readline.createInterface({ input: spawnedProcess.stdout }); 70 | stdout.on('line', line => { 71 | onLineReceivedInStdout(line); 72 | }); 73 | } 74 | if (this.onLineReceivedInStderr !== undefined) { 75 | const onLineReceivedInStderr = this.onLineReceivedInStderr; 76 | const stderr = readline.createInterface({ input: spawnedProcess.stderr }); 77 | stderr.on('line', line => { 78 | onLineReceivedInStderr(line); 79 | }); 80 | } 81 | spawnedProcess.on('error', error => { 82 | reject(error); 83 | }); 84 | spawnedProcess.on('exit', code => { 85 | process.removeAllListeners(); 86 | if (this.process === spawnedProcess) { 87 | this.process = undefined; 88 | } 89 | if (this.interrupted) { 90 | reject(); 91 | return; 92 | } 93 | resolve(code); 94 | }); 95 | }); 96 | } 97 | 98 | public kill(): Thenable { 99 | return new Promise(resolve => { 100 | if (!this.interrupted && this.process) { 101 | kill(this.process.pid, 'SIGTERM', resolve); 102 | this.interrupted = true; 103 | } 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/components/cargo/terminal_task_manager.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, Terminal, window } from 'vscode'; 2 | import { getCommandForArgs, getCommandToChangeWorkingDirectory, getCommandToSetEnvVar } 3 | from '../../CommandLine'; 4 | import { ShellProviderManager } from '../../ShellProviderManager'; 5 | import { Configuration } from '../configuration/Configuration'; 6 | 7 | export class TerminalTaskManager { 8 | private _configuration: Configuration; 9 | private _runningTerminal: Terminal | undefined; 10 | private _shellProvider: ShellProviderManager; 11 | 12 | public constructor( 13 | context: ExtensionContext, 14 | configuration: Configuration, 15 | shellProviderManager: ShellProviderManager 16 | ) { 17 | this._configuration = configuration; 18 | this._shellProvider = shellProviderManager; 19 | context.subscriptions.push( 20 | window.onDidCloseTerminal(closedTerminal => { 21 | if (closedTerminal === this._runningTerminal) { 22 | this._runningTerminal = undefined; 23 | } 24 | }) 25 | ); 26 | } 27 | 28 | /** 29 | * Returns whether some task is running 30 | */ 31 | public hasRunningTask(): boolean { 32 | return this._runningTerminal !== undefined; 33 | } 34 | 35 | public stopRunningTask(): void { 36 | if (this._runningTerminal) { 37 | this._runningTerminal.dispose(); 38 | this._runningTerminal = undefined; 39 | } 40 | } 41 | 42 | public async startTask( 43 | executable: string, 44 | preCommandArgs: string[], 45 | command: string, 46 | args: string[], 47 | cwd: string 48 | ): Promise { 49 | args = preCommandArgs.concat(command, ...args); 50 | const terminal = window.createTerminal('Cargo Task'); 51 | this._runningTerminal = terminal; 52 | const shell = await this._shellProvider.getValue(); 53 | if (shell === undefined) { 54 | return; 55 | } 56 | const setEnvironmentVariables = () => { 57 | const cargoEnv = this._configuration.getCargoEnv(); 58 | // Set environment variables 59 | for (const name in cargoEnv) { 60 | if (name in cargoEnv) { 61 | const value = cargoEnv[name]; 62 | terminal.sendText(getCommandToSetEnvVar(shell, name, value)); 63 | } 64 | } 65 | }; 66 | setEnvironmentVariables(); 67 | // Change the current directory to a specified directory 68 | this._runningTerminal.sendText(getCommandToChangeWorkingDirectory(shell, cwd)); 69 | // Start a requested command 70 | this._runningTerminal.sendText(getCommandForArgs(shell, [executable, ...args])); 71 | this._runningTerminal.show(true); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/components/completion/racer_status_bar_item.ts: -------------------------------------------------------------------------------- 1 | import { StatusBarAlignment, StatusBarItem, window } from 'vscode'; 2 | 3 | export class RacerStatusBarItem { 4 | private showErrorCommandName: string; 5 | 6 | private statusBarItem: StatusBarItem; 7 | 8 | public constructor(showErrorCommandName: string) { 9 | this.showErrorCommandName = showErrorCommandName; 10 | 11 | this.statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left); 12 | } 13 | 14 | public showTurnedOn(): void { 15 | this.setText('On'); 16 | 17 | this.clearCommand(); 18 | 19 | this.statusBarItem.show(); 20 | } 21 | 22 | public showTurnedOff(): void { 23 | this.setText('Off'); 24 | 25 | this.clearCommand(); 26 | 27 | this.statusBarItem.show(); 28 | } 29 | 30 | public showNotFound(): void { 31 | this.setText('Not found'); 32 | 33 | this.clearCommand(); 34 | 35 | this.statusBarItem.tooltip = 36 | 'The "racer" command is not available. Make sure it is installed.'; 37 | this.statusBarItem.show(); 38 | } 39 | 40 | public showCrashed(): void { 41 | this.setText('Crashed'); 42 | 43 | this.statusBarItem.tooltip = 'The racer process has stopped. Click to view error'; 44 | this.statusBarItem.command = this.showErrorCommandName; 45 | this.statusBarItem.show(); 46 | } 47 | 48 | private setText(text: string): void { 49 | this.statusBarItem.text = `Racer: ${text}`; 50 | } 51 | 52 | private clearCommand(): void { 53 | // It is workaround because currently the typoe of StatusBarItem.command is string. 54 | const statusBarItem: any = this.statusBarItem; 55 | statusBarItem.command = undefined; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/configuration/Configuration.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceConfiguration, workspace } from 'vscode'; 2 | import expandTilde = require('expand-tilde'); 3 | import { FileSystem } from '../file_system/FileSystem'; 4 | import { ChildLogger } from '../logging/child_logger'; 5 | 6 | export enum ActionOnStartingCommandIfThereIsRunningCommand { 7 | StopRunningCommand, 8 | IgnoreNewCommand, 9 | ShowDialogToLetUserDecide 10 | } 11 | 12 | export enum Mode { 13 | Legacy, 14 | RLS 15 | } 16 | 17 | /** 18 | * Returns the representation of the specified mode suitable for being a value for the 19 | * configuration parameter 20 | * @param mode The mode which representation will be returned for 21 | * @return The representation of the specified mode 22 | */ 23 | export function asConfigurationParameterValue(mode: Mode | undefined): string | null { 24 | switch (mode) { 25 | case Mode.Legacy: 26 | return 'legacy'; 27 | case Mode.RLS: 28 | return 'rls'; 29 | case undefined: 30 | return null; 31 | } 32 | } 33 | 34 | namespace Properties { 35 | export const mode = 'mode'; 36 | } 37 | 38 | /** 39 | * The main class of the component `Configuration`. 40 | * This class contains code related to Configuration 41 | */ 42 | export class Configuration { 43 | private _mode: Mode | undefined; 44 | private logger: ChildLogger; 45 | 46 | /** 47 | * A path to the executable of racer. 48 | * It contains a value of either: 49 | * - the configuration parameter `rust.racerPath` 50 | * - a path found in any of directories specified in the envirionment variable PATH 51 | * The configuration parameter has higher priority than automatically found path 52 | */ 53 | private racerPath: string | undefined; 54 | 55 | public static getConfiguration(): WorkspaceConfiguration { 56 | const configuration = workspace.getConfiguration('rust'); 57 | 58 | return configuration; 59 | } 60 | 61 | public static getPathConfigParameter(parameterName: string): string | undefined { 62 | const parameter = this.getStringConfigParameter(parameterName); 63 | if (parameter) { 64 | return expandTilde(parameter); 65 | } else { 66 | return undefined; 67 | } 68 | } 69 | 70 | public static getPathConfigParameterOrDefault( 71 | parameterName: string, 72 | defaultValue: string 73 | ): string { 74 | const parameter = this.getPathConfigParameter(parameterName); 75 | if (typeof parameter === 'string') { 76 | return parameter; 77 | } else { 78 | return defaultValue; 79 | } 80 | } 81 | 82 | public static getPathEnvParameter(parameterName: string): string | undefined { 83 | const parameter = process.env[parameterName]; 84 | if (parameter) { 85 | return expandTilde(parameter); 86 | } else { 87 | return undefined; 88 | } 89 | } 90 | 91 | public static getStringConfigParameter(parameterName: string): string | undefined { 92 | const configuration = workspace.getConfiguration('rust'); 93 | const parameter = configuration.get(parameterName); 94 | return parameter; 95 | } 96 | 97 | /** 98 | * Creates a new instance of the class. 99 | * @param logger A value for the field `logger` 100 | */ 101 | public constructor(logger: ChildLogger) { 102 | function mode(): Mode | undefined { 103 | const configuration = Configuration.getConfiguration(); 104 | const value: string | null | undefined = configuration[Properties.mode]; 105 | if (typeof value === 'string') { 106 | switch (value) { 107 | case asConfigurationParameterValue(Mode.Legacy): 108 | return Mode.Legacy; 109 | case asConfigurationParameterValue(Mode.RLS): 110 | return Mode.RLS; 111 | default: 112 | return undefined; 113 | } 114 | } else { 115 | return undefined; 116 | } 117 | } 118 | this._mode = mode(); 119 | this.logger = logger; 120 | this.racerPath = undefined; 121 | } 122 | 123 | /** 124 | * Updates the value of the field `pathToRacer`. 125 | * It checks if a user specified any path in the configuration. 126 | * If no path specified or a specified path can't be used, it finds in directories specified in the environment variable PATH. 127 | * This method is asynchronous because it checks if a path exists before setting it to the field 128 | */ 129 | public async updatePathToRacer(): Promise { 130 | async function findRacerPathSpecifiedByUser(logger: ChildLogger): Promise { 131 | const methodLogger = logger.createChildLogger('findRacerPathSpecifiedByUser: '); 132 | let path: string | undefined | null = Configuration.getPathConfigParameter('racerPath'); 133 | if (!path) { 134 | methodLogger.debug(`path=${path}`); 135 | return undefined; 136 | } 137 | path = expandTilde(path); 138 | methodLogger.debug(`path=${path}`); 139 | const foundPath: string | undefined = await FileSystem.findExecutablePath(path); 140 | methodLogger.debug(`foundPath=${foundPath}`); 141 | return foundPath; 142 | } 143 | async function findDefaultRacerPath(logger: ChildLogger): Promise { 144 | const methodLogger = logger.createChildLogger('findDefaultRacerPath: '); 145 | const foundPath: string | undefined = await FileSystem.findExecutablePath('racer'); 146 | methodLogger.debug(`foundPath=${foundPath}`); 147 | return foundPath; 148 | } 149 | const logger = this.logger.createChildLogger('updatePathToRacer: '); 150 | this.racerPath = ( 151 | await findRacerPathSpecifiedByUser(logger) || 152 | await findDefaultRacerPath(logger) 153 | ); 154 | } 155 | 156 | /** 157 | * Returns the mode which the extension runs in 158 | * @return The mode 159 | */ 160 | public mode(): Mode | undefined { 161 | return this._mode; 162 | } 163 | 164 | /** 165 | * Saves the specified mode in both the object and the configuration 166 | * @param mode The mode 167 | */ 168 | public setMode(mode: Mode | undefined): void { 169 | this._mode = mode; 170 | const configuration = Configuration.getConfiguration(); 171 | configuration.update(Properties.mode, asConfigurationParameterValue(mode), true); 172 | } 173 | 174 | /** 175 | * Returns a value of the field `pathToRacer` 176 | */ 177 | public getPathToRacer(): string | undefined { 178 | return this.racerPath; 179 | } 180 | 181 | public shouldExecuteCargoCommandInTerminal(): boolean { 182 | // When RLS is used any cargo command is executed in an integrated terminal. 183 | if (this.mode() === Mode.RLS) { 184 | return true; 185 | } 186 | const configuration = Configuration.getConfiguration(); 187 | const shouldExecuteCargoCommandInTerminal = configuration['executeCargoCommandInTerminal']; 188 | return shouldExecuteCargoCommandInTerminal; 189 | } 190 | 191 | public getActionOnSave(): string | undefined { 192 | return Configuration.getStringConfigParameter('actionOnSave'); 193 | } 194 | 195 | public shouldShowRunningCargoTaskOutputChannel(): boolean { 196 | const configuration = Configuration.getConfiguration(); 197 | const shouldShowRunningCargoTaskOutputChannel = configuration['showOutput']; 198 | return shouldShowRunningCargoTaskOutputChannel; 199 | } 200 | 201 | public getCargoEnv(): any { 202 | return Configuration.getConfiguration().get('cargoEnv'); 203 | } 204 | 205 | public getCargoCwd(): string | undefined { 206 | return Configuration.getPathConfigParameter('cargoCwd'); 207 | } 208 | 209 | public getCargoHomePath(): string | undefined { 210 | const configPath = Configuration.getPathConfigParameter('cargoHomePath'); 211 | const envPath = Configuration.getPathEnvParameter('CARGO_HOME'); 212 | return configPath || envPath || undefined; 213 | } 214 | 215 | public getRustfmtPath(): string { 216 | return Configuration.getPathConfigParameterOrDefault('rustfmtPath', 'rustfmt'); 217 | } 218 | 219 | public getRustsymPath(): string { 220 | return Configuration.getPathConfigParameterOrDefault('rustsymPath', 'rustsym'); 221 | } 222 | 223 | public getActionOnStartingCommandIfThereIsRunningCommand(): ActionOnStartingCommandIfThereIsRunningCommand { 224 | const configuration = Configuration.getConfiguration(); 225 | const action = configuration['actionOnStartingCommandIfThereIsRunningCommand']; 226 | switch (action) { 227 | case 'Stop running command': 228 | return ActionOnStartingCommandIfThereIsRunningCommand.StopRunningCommand; 229 | case 'Show dialog to let me decide': 230 | return ActionOnStartingCommandIfThereIsRunningCommand.ShowDialogToLetUserDecide; 231 | default: 232 | return ActionOnStartingCommandIfThereIsRunningCommand.IgnoreNewCommand; 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/components/configuration/NotRustup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration of Rust installed not via Rustup, but via other variant 3 | */ 4 | export class NotRustup { 5 | /** 6 | * A path to Rust's installation root. 7 | * It is what `rustc --print=sysroot` returns 8 | */ 9 | private rustcSysRoot: string; 10 | 11 | public constructor(rustcSysRoot: string) { 12 | this.rustcSysRoot = rustcSysRoot; 13 | } 14 | 15 | /** 16 | * Returns Rust's installation root 17 | */ 18 | public getRustcSysRoot(): string { 19 | return this.rustcSysRoot; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/configuration/RlsConfiguration.ts: -------------------------------------------------------------------------------- 1 | import expandTilde = require('expand-tilde'); 2 | import { RevealOutputChannelOn as RevealOutputChannelOnEnum } from 'vscode-languageclient'; 3 | import { ConfigurationParameter } from '../../ConfigurationParameter'; 4 | import { FileSystem } from '../file_system/FileSystem'; 5 | import { Rustup } from './Rustup'; 6 | import { RustSource } from './RustSource'; 7 | 8 | /** 9 | * This class provides functionality related to RLS configuration 10 | */ 11 | export class RlsConfiguration { 12 | private _useRustfmtConfigurationParameter: ConfigurationParameters.UseRustfmt; 13 | private _rustup: Rustup | undefined; 14 | private _rustSource: RustSource; 15 | private _executableUserPath: string | undefined; 16 | private _userArgs: string[]; 17 | private _userEnv: object; 18 | private _revealOutputChannelOn: RevealOutputChannelOnEnum; 19 | private _useRustfmt: boolean | undefined; 20 | 21 | /** 22 | * Creates a new instance of the class 23 | * @param rustup The rustup object 24 | * @param rustSource The rust's source object 25 | */ 26 | public static async create(rustup: Rustup | undefined, rustSource: RustSource): Promise { 27 | const rlsExecutableConfigurationParameter = new ConfigurationParameters.Executable(); 28 | const executableUserPath = await rlsExecutableConfigurationParameter.getCheckedExecutable(); 29 | return new RlsConfiguration(rustup, rustSource, executableUserPath); 30 | } 31 | 32 | /** 33 | * Returns if there is some executable path specified by the user 34 | */ 35 | public isExecutableUserPathSet(): boolean { 36 | return this._executableUserPath !== undefined; 37 | } 38 | 39 | /** 40 | * Returns a path to RLS executable 41 | */ 42 | public getExecutablePath(): string | undefined { 43 | if (this._executableUserPath) { 44 | return this._executableUserPath; 45 | } 46 | if (this._rustup && this._rustup.isRlsInstalled()) { 47 | return 'rustup'; 48 | } 49 | return undefined; 50 | } 51 | 52 | /** 53 | * Returns arguments for RLS 54 | */ 55 | public getArgs(): string[] { 56 | // When the user specifies some executable path, the user expects the extension not to add 57 | // some arguments 58 | if (this._executableUserPath === undefined && this._rustup && this._rustup.isRlsInstalled()) { 59 | const userToolchain = this._rustup.getUserNightlyToolchain(); 60 | if (!userToolchain) { 61 | // It is actually impossible because `isRlsInstalled` uses `getUserNightlyToolchain` 62 | return this._userArgs; 63 | } 64 | return ['run', userToolchain.toString(true, false), 'rls'].concat(this._userArgs); 65 | } else { 66 | return this._userArgs; 67 | } 68 | } 69 | 70 | /** 71 | * Returns environment to run RLS in 72 | */ 73 | public getEnv(): object { 74 | const env: any = Object.assign({}, this._userEnv); 75 | if (!env.RUST_SRC_PATH) { 76 | const rustSourcePath = this._rustSource.getPath(); 77 | if (rustSourcePath) { 78 | env.RUST_SRC_PATH = rustSourcePath; 79 | } 80 | } 81 | return env; 82 | } 83 | 84 | /** 85 | * Returns how the output channel of RLS should behave when receiving messages 86 | */ 87 | public getRevealOutputChannelOn(): RevealOutputChannelOnEnum { 88 | return this._revealOutputChannelOn; 89 | } 90 | 91 | /** 92 | * Returns whether rustfmt should be used for formatting 93 | */ 94 | public getUseRustfmt(): boolean | undefined { 95 | return this._useRustfmt; 96 | } 97 | 98 | /** 99 | * Updates the property "useRustfmt" in the user configuration 100 | * @param value The new value 101 | */ 102 | public setUseRustfmt(value: boolean | undefined): void { 103 | if (this._useRustfmt === value) { 104 | return; 105 | } 106 | this._useRustfmt = value; 107 | this._useRustfmtConfigurationParameter.setUseRustfmt(value); 108 | } 109 | 110 | private constructor(rustup: Rustup | undefined, rustSource: RustSource, executableUserPath: string | undefined) { 111 | this._useRustfmtConfigurationParameter = new ConfigurationParameters.UseRustfmt(); 112 | this._rustup = rustup; 113 | this._rustSource = rustSource; 114 | this._executableUserPath = executableUserPath; 115 | this._userArgs = new ConfigurationParameters.Args().getArgs(); 116 | this._userEnv = new ConfigurationParameters.Env().getEnv(); 117 | this._revealOutputChannelOn = new ConfigurationParameters.RevealOutputChannelOn().getRevealOutputChannelOn(); 118 | this._useRustfmt = this._useRustfmtConfigurationParameter.getUseRustfmt(); 119 | } 120 | } 121 | 122 | namespace ConfigurationParameters { 123 | const RLS_CONFIGURATION_PARAMETER_SECTION = 'rust.rls'; 124 | 125 | /** 126 | * The wrapper around the configuration parameter of the RLS executable 127 | */ 128 | export class Executable { 129 | /** 130 | * The configuration parameter of the RLS executable 131 | */ 132 | private _parameter: ConfigurationParameter; 133 | 134 | public constructor() { 135 | this._parameter = new ConfigurationParameter(RLS_CONFIGURATION_PARAMETER_SECTION, 'executable'); 136 | } 137 | 138 | /** 139 | * Gets the executable from the configuration, checks if it exists and returns it 140 | * @return The existing executable from the configuration 141 | */ 142 | public async getCheckedExecutable(): Promise { 143 | const executable = this._parameter.getValue(); 144 | // It is either string or `null`, but VSCode doens't prevent us from putting a number 145 | // here 146 | if (!(typeof executable === 'string')) { 147 | return undefined; 148 | } 149 | // If we passed the previous check, then it is a string, but it may be an empty string. In 150 | // that case we return `undefined` because an empty string is not valid path 151 | if (!executable) { 152 | return undefined; 153 | } 154 | // The user may input `~/.cargo/bin/rls` and expect it to work 155 | const tildeExpandedExecutable = expandTilde(executable); 156 | // We have to check if the path exists because otherwise the language server wouldn't start 157 | const foundExecutable = await FileSystem.findExecutablePath(tildeExpandedExecutable); 158 | return foundExecutable; 159 | } 160 | } 161 | 162 | /** 163 | * The wrapper around the configuration parameter of the RLS arguments 164 | */ 165 | export class Args { 166 | /** 167 | * The configuration parameter of the RLS arguments 168 | */ 169 | private _parameter: ConfigurationParameter; 170 | 171 | public constructor() { 172 | this._parameter = new ConfigurationParameter(RLS_CONFIGURATION_PARAMETER_SECTION, 'args'); 173 | } 174 | 175 | /** 176 | * Gets the arguments from the configuration and returns them 177 | * @return The arguments 178 | */ 179 | public getArgs(): string[] { 180 | const args = this._parameter.getValue(); 181 | // It is either array or `null`, but VSCode doens't prevent us from putting a number 182 | // here 183 | if (!(args instanceof Array)) { 184 | return []; 185 | } 186 | return args; 187 | } 188 | } 189 | 190 | /** 191 | * The wrapper around the configuration parameter of the RLS environment 192 | */ 193 | export class Env { 194 | /** 195 | * The configuration parameter of the RLS environment 196 | */ 197 | private _parameter: ConfigurationParameter; 198 | 199 | public constructor() { 200 | this._parameter = new ConfigurationParameter(RLS_CONFIGURATION_PARAMETER_SECTION, 'env'); 201 | } 202 | 203 | /** 204 | * Gets the environment from the configuration and returns it 205 | * @return The environment 206 | */ 207 | public getEnv(): object { 208 | const env = this._parameter.getValue(); 209 | // It is either object or `null`, but VSCode doens't prevent us from putting a number 210 | // here 211 | if (!(typeof env === 'object')) { 212 | return {}; 213 | } 214 | return env; 215 | } 216 | } 217 | 218 | /** 219 | * The wrapper around the configuration parameter specifying on what kind of message the output 220 | * channel is revealed 221 | */ 222 | export class RevealOutputChannelOn { 223 | /** 224 | * The configuration parameter specifying on what kind of message the output channel is 225 | * revealed 226 | */ 227 | private _parameter: ConfigurationParameter; 228 | 229 | public constructor() { 230 | this._parameter = new ConfigurationParameter(RLS_CONFIGURATION_PARAMETER_SECTION, 'revealOutputChannelOn'); 231 | } 232 | 233 | /** 234 | * Gets the value specifying on what kind of message the output channel is revealed from 235 | * the configuration and returns it 236 | * @return The environment 237 | */ 238 | public getRevealOutputChannelOn(): RevealOutputChannelOnEnum { 239 | const revealOutputChannelOn = this._parameter.getValue(); 240 | switch (revealOutputChannelOn) { 241 | case 'info': 242 | return RevealOutputChannelOnEnum.Info; 243 | case 'warn': 244 | return RevealOutputChannelOnEnum.Warn; 245 | case 'error': 246 | return RevealOutputChannelOnEnum.Error; 247 | case 'never': 248 | return RevealOutputChannelOnEnum.Never; 249 | default: 250 | return RevealOutputChannelOnEnum.Error; 251 | } 252 | } 253 | } 254 | 255 | /** 256 | * The wrapper around the configuration parameter specifying if rustfmt is used to format code 257 | */ 258 | export class UseRustfmt { 259 | /** 260 | * The configuration parameter specifying if rustfmt is used to format code 261 | */ 262 | private _parameter: ConfigurationParameter; 263 | 264 | public constructor() { 265 | this._parameter = new ConfigurationParameter(RLS_CONFIGURATION_PARAMETER_SECTION, 'useRustfmt'); 266 | } 267 | 268 | /** 269 | * Gets the flag specifying if rustfmt is used to format code from the configuration and returns it 270 | * @return The environment 271 | */ 272 | public getUseRustfmt(): boolean | undefined { 273 | const useRustfmt = this._parameter.getValue(); 274 | // It is either booleans or `null`, but VSCode doens't prevent us from putting a number 275 | // here 276 | if (!(typeof useRustfmt === 'boolean')) { 277 | return undefined; 278 | } 279 | return useRustfmt; 280 | } 281 | 282 | /** 283 | * Sets the value to the configuration 284 | * @param useRustfmt The new value 285 | */ 286 | public setUseRustfmt(useRustfmt: boolean | undefined): void { 287 | this._parameter.setValue(useRustfmt, true); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/components/configuration/RustSource.ts: -------------------------------------------------------------------------------- 1 | import { FileSystem } from '../file_system/FileSystem'; 2 | import { Configuration } from './Configuration'; 3 | import { Rustup } from './Rustup'; 4 | 5 | /** 6 | * This class provides functionality related to Rust's source 7 | */ 8 | export class RustSource { 9 | private _path: string | undefined; 10 | 11 | /** 12 | * Creates a new instance of the class 13 | * @param rustup The rustup object 14 | */ 15 | public static async create(rustup: Rustup | undefined): Promise { 16 | const path = await getPath(rustup); 17 | return new RustSource(path); 18 | } 19 | 20 | /** 21 | * Returns the path 22 | */ 23 | public getPath(): string | undefined { 24 | return this._path; 25 | } 26 | 27 | private constructor(path: string | undefined) { 28 | this._path = path; 29 | } 30 | } 31 | 32 | async function getPath(rustup: Rustup | undefined): Promise { 33 | const userPath = await getUserPath(); 34 | if (userPath) { 35 | return userPath; 36 | } 37 | if (rustup) { 38 | return rustup.getPathToRustSourceCode(); 39 | } else { 40 | return undefined; 41 | } 42 | } 43 | 44 | async function getUserPath(): Promise { 45 | const configurationPath = await getConfigurationPath(); 46 | if (configurationPath) { 47 | return configurationPath; 48 | } 49 | return await getEnvPath(); 50 | } 51 | 52 | async function checkPath(path: string | undefined): Promise { 53 | if (!path) { 54 | return undefined; 55 | } 56 | if (await FileSystem.doesPathExist(path)) { 57 | return path; 58 | } else { 59 | return undefined; 60 | } 61 | } 62 | 63 | async function getConfigurationPath(): Promise { 64 | const path = Configuration.getPathConfigParameter('rustLangSrcPath'); 65 | return await checkPath(path); 66 | } 67 | 68 | async function getEnvPath(): Promise { 69 | const path = Configuration.getPathEnvParameter('RUST_SRC_PATH'); 70 | return await checkPath(path); 71 | } 72 | -------------------------------------------------------------------------------- /src/components/configuration/current_working_directory_manager.ts: -------------------------------------------------------------------------------- 1 | import { access } from 'fs'; 2 | 3 | import { dirname, join } from 'path'; 4 | 5 | import { window, workspace } from 'vscode'; 6 | 7 | import findUp = require('find-up'); 8 | 9 | export class CurrentWorkingDirectoryManager { 10 | private rememberedCwd: string | undefined; 11 | 12 | public cwd(): Promise { 13 | // Internal description of the method: 14 | // Issue: https://github.com/KalitaAlexey/vscode-rust/issues/36 15 | // The algorithm: 16 | // * Try finding cwd out of an active text editor 17 | // * If it succeeds: 18 | // * Remember the cwd for later use when for some reasons 19 | // a cwd wouldn't be find out of an active text editor 20 | // * Otherwise: 21 | // * Try using a previous cwd 22 | // * If there is previous cwd: 23 | // * Use it 24 | // * Otherwise: 25 | // * Try using workspace as cwd 26 | 27 | return this.getCwdFromActiveTextEditor() 28 | .then(newCwd => { 29 | this.rememberedCwd = newCwd; 30 | 31 | return newCwd; 32 | }) 33 | .catch((error: Error) => { 34 | return this.getPreviousCwd(error); 35 | }) 36 | .catch((error: Error) => { 37 | return this.checkWorkspaceCanBeUsedAsCwd().then(canBeUsed => { 38 | if (canBeUsed) { 39 | return Promise.resolve(workspace.rootPath); 40 | } else { 41 | return Promise.reject(error); 42 | } 43 | }); 44 | }); 45 | } 46 | 47 | private checkWorkspaceCanBeUsedAsCwd(): Promise { 48 | if (!workspace.rootPath) { 49 | return Promise.resolve(false); 50 | } 51 | 52 | const filePath = join(workspace.rootPath, 'Cargo.toml'); 53 | 54 | return this.checkPathExists(filePath); 55 | } 56 | 57 | private getCwdFromActiveTextEditor(): Promise { 58 | if (!window.activeTextEditor) { 59 | return Promise.reject(new Error('No active document')); 60 | } 61 | 62 | const fileName = window.activeTextEditor.document.fileName; 63 | 64 | if (!workspace.rootPath || !fileName.startsWith(workspace.rootPath)) { 65 | return Promise.reject(new Error('Current document not in the workspace')); 66 | } 67 | 68 | return this.findCargoTomlUpToWorkspace(dirname(fileName)); 69 | } 70 | 71 | private findCargoTomlUpToWorkspace(cwd: string): Promise { 72 | const opts = { cwd: cwd }; 73 | 74 | return findUp('Cargo.toml', opts).then((cargoTomlDirPath: string) => { 75 | if (!cargoTomlDirPath) { 76 | return Promise.reject(new Error('Cargo.toml hasn\'t been found')); 77 | } 78 | 79 | if (!workspace.rootPath || !cargoTomlDirPath.startsWith(workspace.rootPath)) { 80 | return Promise.reject(new Error('Cargo.toml hasn\'t been found within the workspace')); 81 | } 82 | 83 | return Promise.resolve(dirname(cargoTomlDirPath)); 84 | }); 85 | } 86 | 87 | private getPreviousCwd(error: Error): Promise { 88 | if (!this.rememberedCwd) { 89 | return Promise.reject(error); 90 | } 91 | 92 | const pathToCargoTomlInPreviousCwd = join(this.rememberedCwd, 'Cargo.toml'); 93 | 94 | return this.checkPathExists(pathToCargoTomlInPreviousCwd).then(exists => { 95 | if (exists) { 96 | return Promise.resolve(this.rememberedCwd); 97 | } else { 98 | return Promise.reject(error); 99 | } 100 | }); 101 | } 102 | 103 | private checkPathExists(path: string): Promise { 104 | return new Promise(resolve => { 105 | access(path, e => { 106 | // A path exists if there is no error 107 | const pathExists = !e; 108 | 109 | resolve(pathExists); 110 | }); 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/components/configuration/mod.ts: -------------------------------------------------------------------------------- 1 | import { DocumentFilter } from 'vscode'; 2 | 3 | export function getDocumentFilter(): DocumentFilter { 4 | return { language: 'rust', scheme: 'file' }; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/file_system/FileSystem.ts: -------------------------------------------------------------------------------- 1 | import { access } from 'fs'; 2 | 3 | import which = require('which'); 4 | 5 | /** 6 | * Code related to file system 7 | */ 8 | export class FileSystem { 9 | /** 10 | * Checks if there is a file or a directory at a specified path 11 | * @param path a path to check 12 | * @return true if there is a file or a directory otherwise false 13 | */ 14 | public static doesPathExist(path: string): Promise { 15 | return new Promise(resolve => { 16 | access(path, err => { 17 | const pathExists = !err; 18 | 19 | resolve(pathExists); 20 | }); 21 | }); 22 | } 23 | 24 | /** 25 | * Looks for a specified executable at paths specified in the environment variable PATH 26 | * @param executable an executable to look for 27 | * @return A path to the executable if it has been found otherwise undefined 28 | */ 29 | public static async findExecutablePath(executable: string): Promise { 30 | return new Promise(resolve => { 31 | which(executable, (err, path) => { 32 | if (err) { 33 | resolve(undefined); 34 | } else { 35 | resolve(path); 36 | } 37 | }); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/formatting/formatting_manager.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import * as fs from 'fs'; 3 | import { 4 | DocumentFormattingEditProvider, 5 | DocumentRangeFormattingEditProvider, 6 | ExtensionContext, 7 | Range, 8 | TextDocument, 9 | TextEdit, 10 | Uri, 11 | languages, 12 | window 13 | } from 'vscode'; 14 | import { Configuration } from '../configuration/Configuration'; 15 | import { getDocumentFilter } from '../configuration/mod'; 16 | import { FileSystem } from '../file_system/FileSystem'; 17 | import { ChildLogger } from '../logging/child_logger'; 18 | 19 | const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; 20 | 21 | interface RustFmtDiff { 22 | startLine: number; 23 | newLines: string[]; 24 | removedLines: number; 25 | } 26 | 27 | export class FormattingManager implements DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider { 28 | private _newFormatRegex: RegExp = /^Diff in (.*) at line (\d+):$/; 29 | private _configuration: Configuration; 30 | private _logger: ChildLogger; 31 | 32 | public static async create( 33 | context: ExtensionContext, 34 | configuration: Configuration, 35 | logger: ChildLogger 36 | ): Promise { 37 | const rustfmtPath: string | undefined = await FileSystem.findExecutablePath(configuration.getRustfmtPath()); 38 | if (rustfmtPath === undefined) { 39 | return undefined; 40 | } 41 | return new FormattingManager(context, configuration, logger); 42 | } 43 | 44 | public provideDocumentFormattingEdits(document: TextDocument): Thenable { 45 | return this.formattingEdits(document); 46 | } 47 | 48 | public provideDocumentRangeFormattingEdits(document: TextDocument, range: Range): Thenable { 49 | return this.formattingEdits(document, range); 50 | } 51 | 52 | /** 53 | * To create an instance of the class use the method `create` 54 | * @param context The extension context 55 | * @param configuration The configuration 56 | * @param logger the logger used to create a child logger to log messages 57 | */ 58 | private constructor( 59 | context: ExtensionContext, 60 | configuration: Configuration, 61 | logger: ChildLogger 62 | ) { 63 | this._configuration = configuration; 64 | this._logger = logger.createChildLogger('FormattingManager: '); 65 | context.subscriptions.push( 66 | languages.registerDocumentFormattingEditProvider( 67 | getDocumentFilter(), 68 | this 69 | ), 70 | languages.registerDocumentRangeFormattingEditProvider( 71 | getDocumentFilter(), 72 | this 73 | ) 74 | ); 75 | } 76 | 77 | private formattingEdits(document: TextDocument, range?: Range): Thenable { 78 | const logger = this._logger.createChildLogger('formattingEdits: '); 79 | return new Promise((resolve, reject) => { 80 | const fileName = document.fileName + '.fmt'; 81 | fs.writeFileSync(fileName, document.getText()); 82 | const rustfmtPath = this._configuration.getRustfmtPath(); 83 | logger.debug(`rustfmtPath=${rustfmtPath}`); 84 | const args = ['--skip-children', '--write-mode=diff']; 85 | if (range !== undefined) { 86 | args.push('--file-lines', 87 | `[{"file":"${fileName}","range":[${range.start.line + 1}, ${range.end.line + 1}]}]`); 88 | } else { 89 | args.push(fileName); 90 | } 91 | logger.debug(`args=${JSON.stringify(args)}`); 92 | const env = Object.assign({ TERM: 'xterm' }, process.env); 93 | cp.execFile(rustfmtPath, args, { env: env }, (err, stdout, stderr) => { 94 | try { 95 | if (err && (err).code === 'ENOENT') { 96 | window.showInformationMessage('The "rustfmt" command is not available. Make sure it is installed.'); 97 | return resolve([]); 98 | } 99 | 100 | // rustfmt will return with exit code 3 when it encounters code that could not 101 | // be automatically formatted. However, it will continue to format the rest of the file. 102 | // New releases will return exit code 4 when the write mode is diff and a valid diff is provided. 103 | // For these reasons, if the exit code is 1 or 2, then it should be treated as an error. 104 | const hasFatalError = (err && (err as any).code < 3); 105 | 106 | if ((err || stderr.length) && hasFatalError) { 107 | logger.debug('cannot format due to syntax errors'); 108 | window.setStatusBarMessage('$(alert) Cannot format due to syntax errors', 5000); 109 | return reject(); 110 | } 111 | 112 | return resolve(this.parseDiff(document.uri, stdout)); 113 | } catch (e) { 114 | logger.error(`e=${e}`); 115 | reject(e); 116 | } finally { 117 | fs.unlinkSync(fileName); 118 | } 119 | }); 120 | }); 121 | } 122 | 123 | private cleanDiffLine(line: string): string { 124 | if (line.endsWith('\u23CE')) { 125 | return line.slice(1, -1) + '\n'; 126 | } 127 | 128 | return line.slice(1); 129 | } 130 | 131 | private stripColorCodes(input: string): string { 132 | return input.replace(ansiRegex, ''); 133 | } 134 | 135 | private parseDiffOldFormat(fileToProcess: Uri, diff: string): RustFmtDiff[] { 136 | const patches: RustFmtDiff[] = []; 137 | let currentPatch: RustFmtDiff | undefined = undefined; 138 | let currentFile: Uri | undefined = undefined; 139 | 140 | for (const line of diff.split(/\n/)) { 141 | if (line.startsWith('Diff of')) { 142 | currentFile = Uri.file(line.slice('Diff of '.length, -1)); 143 | } 144 | 145 | if (!currentFile) { 146 | continue; 147 | } 148 | 149 | if (currentFile.toString() !== fileToProcess.toString() + '.fmt') { 150 | continue; 151 | } 152 | 153 | if (line.startsWith('Diff at line')) { 154 | if (currentPatch != null) { 155 | patches.push(currentPatch); 156 | } 157 | 158 | currentPatch = { 159 | startLine: parseInt(line.slice('Diff at line'.length), 10), 160 | newLines: [], 161 | removedLines: 0 162 | }; 163 | } else if (currentPatch !== undefined) { 164 | if (line.startsWith('+')) { 165 | currentPatch.newLines.push(this.cleanDiffLine(line)); 166 | } else if (line.startsWith('-')) { 167 | currentPatch.removedLines += 1; 168 | } else if (line.startsWith(' ')) { 169 | currentPatch.newLines.push(this.cleanDiffLine(line)); 170 | currentPatch.removedLines += 1; 171 | } 172 | } 173 | } 174 | 175 | if (currentPatch) { 176 | patches.push(currentPatch); 177 | } 178 | 179 | return patches; 180 | } 181 | 182 | private parseDiffNewFormat(fileToProcess: Uri, diff: string): RustFmtDiff[] { 183 | const patches: RustFmtDiff[] = []; 184 | let currentPatch: RustFmtDiff | undefined = undefined; 185 | let currentFilePath: string | undefined = undefined; 186 | const fileToProcessPath = Uri.file(fileToProcess.path + '.fmt').fsPath; 187 | 188 | for (const line of diff.split(/\n/)) { 189 | if (line.startsWith('Diff in')) { 190 | const matches = this._newFormatRegex.exec(line); 191 | 192 | if (!matches) { 193 | continue; 194 | } 195 | 196 | // Filter out malformed lines 197 | if (matches.length !== 3) { 198 | continue; 199 | } 200 | 201 | // If we begin a new diff while already building one, push it as its now complete 202 | if (currentPatch !== undefined) { 203 | patches.push(currentPatch); 204 | } 205 | 206 | // The .path uncanonicalizes the path, which then gets turned into a Uri. 207 | // The .fsPath on both the current file and file to process fix the remaining differences. 208 | currentFilePath = Uri.file(Uri.file(matches[1]).path).fsPath; 209 | currentPatch = { 210 | startLine: parseInt(matches[2], 10), 211 | newLines: [], 212 | removedLines: 0 213 | }; 214 | } 215 | 216 | // We haven't managed to figure out what file we're diffing yet, this shouldn't happen. 217 | // Probably a malformed diff. 218 | if (!currentFilePath) { 219 | continue; 220 | } 221 | 222 | if (currentFilePath !== fileToProcessPath) { 223 | continue; 224 | } 225 | 226 | if (!currentPatch) { 227 | continue; 228 | } 229 | 230 | if (line.startsWith('+')) { 231 | currentPatch.newLines.push(this.cleanDiffLine(line)); 232 | } else if (line.startsWith('-')) { 233 | currentPatch.removedLines += 1; 234 | } else if (line.startsWith(' ')) { 235 | currentPatch.newLines.push(this.cleanDiffLine(line)); 236 | currentPatch.removedLines += 1; 237 | } 238 | } 239 | 240 | // We've reached the end of the data, push the current patch if we were building one 241 | if (currentPatch) { 242 | patches.push(currentPatch); 243 | } 244 | 245 | return patches; 246 | } 247 | 248 | private parseDiff(fileToProcess: Uri, diff: string): TextEdit[] { 249 | diff = this.stripColorCodes(diff); 250 | 251 | let patches: RustFmtDiff[] = []; 252 | const oldFormat = diff.startsWith('Diff of'); 253 | if (oldFormat) { 254 | patches = this.parseDiffOldFormat(fileToProcess, diff); 255 | } else { 256 | patches = this.parseDiffNewFormat(fileToProcess, diff); 257 | } 258 | 259 | let cummulativeOffset = 0; 260 | const textEdits = patches.map(patch => { 261 | const newLines = patch.newLines; 262 | const removedLines = patch.removedLines; 263 | 264 | const startLine = patch.startLine - 1 + cummulativeOffset; 265 | const endLine = removedLines === 0 ? startLine : startLine + removedLines - 1; 266 | const range = new Range(startLine, 0, endLine, Number.MAX_SAFE_INTEGER); 267 | 268 | cummulativeOffset += (removedLines - newLines.length); 269 | 270 | const lastLineIndex = newLines.length - 1; 271 | newLines[lastLineIndex] = newLines[lastLineIndex].replace('\n', ''); 272 | 273 | return TextEdit.replace(range, newLines.join('')); 274 | }); 275 | return textEdits; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/components/language_client/creator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CloseAction, 3 | ErrorAction, 4 | ErrorHandler as IErrorHandler, 5 | LanguageClient, 6 | LanguageClientOptions as ClientOptions, 7 | RevealOutputChannelOn, 8 | ServerOptions 9 | } from 'vscode-languageclient'; 10 | 11 | class ErrorHandler implements IErrorHandler { 12 | private onClosed: () => void; 13 | 14 | public constructor(onClosed: () => void) { 15 | this.onClosed = onClosed; 16 | } 17 | 18 | public error(): ErrorAction { 19 | return ErrorAction.Continue; 20 | } 21 | 22 | public closed(): CloseAction { 23 | this.onClosed(); 24 | 25 | return CloseAction.DoNotRestart; 26 | } 27 | } 28 | 29 | export class Creator { 30 | private clientOptions: ClientOptions; 31 | 32 | private serverOptions: ServerOptions; 33 | 34 | public constructor( 35 | executable: string, 36 | args: string[] | undefined, 37 | env: any | undefined, 38 | revealOutputChannelOn: RevealOutputChannelOn, 39 | onClosed: () => void 40 | ) { 41 | this.clientOptions = { 42 | documentSelector: ['rust'], 43 | revealOutputChannelOn, 44 | errorHandler: new ErrorHandler(onClosed) 45 | }; 46 | 47 | this.serverOptions = { 48 | command: executable, 49 | args, 50 | options: { 51 | env: Object.assign({}, process.env, env ? env : {}) 52 | } 53 | }; 54 | } 55 | 56 | public create(): LanguageClient { 57 | return new LanguageClient('Rust Language Server', this.serverOptions, this.clientOptions); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/language_client/manager.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, ExtensionContext, window, workspace } from 'vscode'; 2 | import { LanguageClient, RevealOutputChannelOn, State } from 'vscode-languageclient'; 3 | import { ChildLogger } from '../logging/child_logger'; 4 | import { Creator as LanguageClientCreator } from './creator'; 5 | import { StatusBarItem } from './status_bar_item'; 6 | 7 | export class Manager { 8 | private languageClientCreator: LanguageClientCreator; 9 | private languageClient: LanguageClient; 10 | private statusBarItem: StatusBarItem; 11 | private logger: ChildLogger; 12 | 13 | public constructor( 14 | context: ExtensionContext, 15 | logger: ChildLogger, 16 | executable: string, 17 | args: string[] | undefined, 18 | env: any | undefined, 19 | revealOutputChannelOn: RevealOutputChannelOn 20 | ) { 21 | this.languageClientCreator = new LanguageClientCreator( 22 | executable, 23 | args, 24 | env, 25 | revealOutputChannelOn, 26 | () => { 27 | this.statusBarItem.setText('Crashed'); 28 | } 29 | ); 30 | this.languageClient = this.languageClientCreator.create(); 31 | this.statusBarItem = new StatusBarItem(context); 32 | this.statusBarItem.setOnClicked(() => { 33 | this.restart(); 34 | }); 35 | this.logger = logger; 36 | this.subscribeOnStateChanging(); 37 | context.subscriptions.push(new Disposable(() => { 38 | this.stop(); 39 | })); 40 | } 41 | 42 | /** 43 | * Starts the language client at first time 44 | */ 45 | public initialStart(): void { 46 | this.start(); 47 | this.statusBarItem.show(); 48 | } 49 | 50 | private start(): void { 51 | this.logger.debug('start'); 52 | this.languageClient.start(); 53 | // As we started the language client, we need to enable the indicator in order to allow the user restart the language client. 54 | this.statusBarItem.setText('Starting'); 55 | this.statusBarItem.enable(); 56 | } 57 | 58 | private async stop(): Promise { 59 | this.logger.debug('stop'); 60 | this.statusBarItem.disable(); 61 | this.statusBarItem.setText('Stopping'); 62 | if (this.languageClient.needsStop()) { 63 | await this.languageClient.stop(); 64 | } 65 | this.languageClient.outputChannel.dispose(); 66 | this.statusBarItem.setText('Stopped'); 67 | } 68 | 69 | /** Stops the running language client if any and starts a new one. */ 70 | private async restart(): Promise { 71 | const isAnyDocumentDirty = !workspace.textDocuments.every(t => !t.isDirty); 72 | if (isAnyDocumentDirty) { 73 | window.showErrorMessage('You have unsaved changes. Save or discard them and try to restart again'); 74 | return; 75 | } 76 | await this.stop(); 77 | this.languageClient = this.languageClientCreator.create(); 78 | this.subscribeOnStateChanging(); 79 | this.start(); 80 | } 81 | 82 | private subscribeOnStateChanging(): void { 83 | this.languageClient.onDidChangeState(event => { 84 | if (event.newState === State.Running) { 85 | this.languageClient.onNotification('rustDocument/diagnosticsBegin', () => { 86 | this.statusBarItem.setText('Analysis started'); 87 | }); 88 | this.languageClient.onNotification('rustDocument/diagnosticsEnd', () => { 89 | this.statusBarItem.setText('Analysis finished'); 90 | }); 91 | } 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/language_client/status_bar_item.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { ExtensionContext, commands, window } from 'vscode'; 4 | 5 | const command = 'rust.LanguageClient.StatusBarItem.Clicked'; 6 | 7 | export class StatusBarItem { 8 | private statusBarItem: vscode.StatusBarItem; 9 | private onClicked?: () => void; 10 | 11 | public constructor(context: ExtensionContext) { 12 | this.statusBarItem = window.createStatusBarItem(); 13 | 14 | context.subscriptions.push( 15 | commands.registerCommand(command, () => { 16 | if (this.onClicked !== undefined) { 17 | this.onClicked(); 18 | } 19 | }) 20 | ); 21 | } 22 | 23 | /** 24 | * Disallows the user to click on the indicator 25 | */ 26 | public disable(): void { 27 | // There is an error in the definition of StatusBarItem.command. 28 | // The expected type is `string | undefined`, but actual is `string`. 29 | // This is workaround. 30 | const statusBarItem: any = this.statusBarItem; 31 | // Disable clicking. 32 | statusBarItem.command = undefined; 33 | // Remove tooltip because we don't want to say the user that we may click on the indicator which is disabled 34 | statusBarItem.tooltip = undefined; 35 | } 36 | 37 | /** 38 | * Allows the user to click on the indicator 39 | */ 40 | public enable(): void { 41 | this.statusBarItem.command = command; 42 | this.statusBarItem.tooltip = 'Click to restart'; 43 | } 44 | 45 | /** 46 | * Saves the specified closure as a closure which is invoked when the user clicks on the indicator 47 | * @param onClicked closure to be invoked 48 | */ 49 | public setOnClicked(onClicked: () => void | undefined): void { 50 | this.onClicked = onClicked; 51 | } 52 | 53 | /** 54 | * Makes the indicator show the specified text in the format "RLS: ${text}" 55 | * @param text the text to be shown 56 | */ 57 | public setText(text: string): void { 58 | this.statusBarItem.text = `RLS: ${text}`; 59 | } 60 | 61 | /** 62 | * Shows the indicator in the status bar 63 | */ 64 | public show(): void { 65 | this.statusBarItem.show(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/logging/ILogger.ts: -------------------------------------------------------------------------------- 1 | import { CapturedMessage } from './captured_message'; 2 | 3 | export interface ILogger { 4 | createChildLogger(loggingMessagePrefix: (() => string) | string): ILogger; 5 | debug(message: string): void; 6 | error(message: string): void; 7 | warning(message: string): void; 8 | startMessageCapture(): void; 9 | takeCapturedMessages(): CapturedMessage[]; 10 | stopMessageCaptureAndReleaseCapturedMessages(): void; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/logging/captured_message.ts: -------------------------------------------------------------------------------- 1 | export enum CapturedMessageSeverity { 2 | Debug, 3 | Error, 4 | Warning 5 | } 6 | 7 | export interface CapturedMessage { 8 | severity: CapturedMessageSeverity; 9 | 10 | message: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/logging/child_logger.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from './logger'; 2 | 3 | export class ChildLogger extends Logger { 4 | private parent: Logger; 5 | 6 | public constructor(loggingMessagePrefix: (() => string) | string, parent: Logger) { 7 | super(loggingMessagePrefix); 8 | 9 | this.parent = parent; 10 | } 11 | 12 | public createChildLogger(loggingMessagePrefix: (() => string) | string): ChildLogger { 13 | return new ChildLogger(loggingMessagePrefix, this); 14 | } 15 | 16 | protected debugProtected(message: string): void { 17 | this.parent.debug(this.getLoggingMessagePrefix().concat(message)); 18 | } 19 | 20 | protected errorProtected(message: string): void { 21 | this.parent.error(this.getLoggingMessagePrefix().concat(message)); 22 | } 23 | 24 | protected warningProtected(message: string): void { 25 | this.parent.warning(this.getLoggingMessagePrefix().concat(message)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/logging/logger.ts: -------------------------------------------------------------------------------- 1 | import { CapturedMessage, CapturedMessageSeverity } from './captured_message'; 2 | import { ILogger } from './ILogger'; 3 | 4 | export abstract class Logger implements ILogger { 5 | private loggingMessagePrefix: (() => string) | string; 6 | 7 | private messageCaptureEnabled: boolean; 8 | 9 | private capturedMessages: CapturedMessage[]; 10 | 11 | public abstract createChildLogger(loggingMessagePrefix: (() => string) | string): ILogger; 12 | 13 | public debug(message: string): void { 14 | const log = this.debugProtected.bind(this); 15 | 16 | this.addMessageToCapturedMessagesOrLog(message, CapturedMessageSeverity.Debug, log); 17 | } 18 | 19 | public error(message: string): void { 20 | const log = this.errorProtected.bind(this); 21 | 22 | this.addMessageToCapturedMessagesOrLog(message, CapturedMessageSeverity.Error, log); 23 | } 24 | 25 | public warning(message: string): void { 26 | const log = this.warningProtected.bind(this); 27 | 28 | this.addMessageToCapturedMessagesOrLog(message, CapturedMessageSeverity.Warning, log); 29 | } 30 | 31 | public startMessageCapture(): void { 32 | this.messageCaptureEnabled = true; 33 | } 34 | 35 | public takeCapturedMessages(): CapturedMessage[] { 36 | const messages = this.capturedMessages; 37 | 38 | this.capturedMessages = []; 39 | 40 | return messages; 41 | } 42 | 43 | public stopMessageCaptureAndReleaseCapturedMessages(): void { 44 | this.messageCaptureEnabled = false; 45 | 46 | const messages = this.takeCapturedMessages(); 47 | 48 | for (const message of messages) { 49 | switch (message.severity) { 50 | case CapturedMessageSeverity.Debug: 51 | this.debug(message.message); 52 | break; 53 | 54 | case CapturedMessageSeverity.Error: 55 | this.error(message.message); 56 | break; 57 | 58 | case CapturedMessageSeverity.Warning: 59 | this.warning(message.message); 60 | break; 61 | 62 | default: 63 | throw new Error(`Unhandled severity=${message.severity}`); 64 | } 65 | } 66 | } 67 | 68 | protected constructor(loggingMessagePrefix: (() => string) | string) { 69 | this.loggingMessagePrefix = loggingMessagePrefix; 70 | 71 | this.messageCaptureEnabled = false; 72 | 73 | this.capturedMessages = []; 74 | } 75 | 76 | protected abstract debugProtected(message: string): void; 77 | 78 | protected abstract errorProtected(message: string): void; 79 | 80 | protected abstract warningProtected(message: string): void; 81 | 82 | protected getLoggingMessagePrefix(): string { 83 | if (typeof this.loggingMessagePrefix === 'string') { 84 | return this.loggingMessagePrefix; 85 | } 86 | 87 | return this.loggingMessagePrefix(); 88 | } 89 | 90 | private addMessageToCapturedMessagesOrLog( 91 | message: string, 92 | severity: CapturedMessageSeverity, 93 | log: (message: string) => void 94 | ): void { 95 | if (this.messageCaptureEnabled) { 96 | this.capturedMessages.push({ 97 | severity: severity, 98 | message: message 99 | }); 100 | } else { 101 | log(message); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/components/logging/logging_manager.ts: -------------------------------------------------------------------------------- 1 | import { OutputChannel, window } from 'vscode'; 2 | import { RootLogger } from './root_logger'; 3 | 4 | export class LoggingManager { 5 | private channel: OutputChannel; 6 | 7 | private logger: RootLogger; 8 | 9 | public constructor() { 10 | this.channel = window.createOutputChannel('Rust logging'); 11 | 12 | this.logger = new RootLogger(''); 13 | 14 | this.logger.setLogFunction((message: string) => { 15 | this.channel.appendLine(message); 16 | }); 17 | } 18 | 19 | public getLogger(): RootLogger { 20 | return this.logger; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/logging/root_logger.ts: -------------------------------------------------------------------------------- 1 | import { ChildLogger } from './child_logger'; 2 | 3 | import { Logger } from './logger'; 4 | 5 | export const DEBUG_MESSAGE_PREFIX = 'DEBUG: '; 6 | 7 | export const ERROR_MESSAGE_PREFIX = 'ERROR: '; 8 | 9 | export const WARNING_MESSAGE_PREFIX = 'WARNING: '; 10 | 11 | export class RootLogger extends Logger { 12 | private logFunction: ((message: string) => void) | undefined; 13 | 14 | public constructor(loggingMessagePrefix: ((() => string) | string)) { 15 | super(loggingMessagePrefix); 16 | 17 | this.logFunction = undefined; 18 | } 19 | 20 | public createChildLogger(loggingMessagePrefix: (() => string) | string): ChildLogger { 21 | return new ChildLogger(loggingMessagePrefix, this); 22 | } 23 | 24 | public setLogFunction(logFunction: ((message: string) => void) | undefined): void { 25 | this.logFunction = logFunction; 26 | } 27 | 28 | protected debugProtected(message: string): void { 29 | this.log(message, DEBUG_MESSAGE_PREFIX); 30 | } 31 | 32 | protected errorProtected(message: string): void { 33 | this.log(message, ERROR_MESSAGE_PREFIX); 34 | } 35 | 36 | protected warningProtected(message: string): void { 37 | this.log(message, WARNING_MESSAGE_PREFIX); 38 | } 39 | 40 | private log(message: string, severityAsString: string): void { 41 | if (!this.logFunction) { 42 | return; 43 | } 44 | 45 | const fullMessage = severityAsString.concat(this.getLoggingMessagePrefix(), message); 46 | 47 | this.logFunction(fullMessage); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/symbol_provision/document_symbol_provision_manager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentSymbolProvider, 3 | ExtensionContext, 4 | SymbolInformation, 5 | TextDocument, 6 | languages 7 | } from 'vscode'; 8 | import { Configuration } from '../configuration/Configuration'; 9 | import { getDocumentFilter } from '../configuration/mod'; 10 | import { SymbolSearchManager } from './symbol_search_manager'; 11 | 12 | export class DocumentSymbolProvisionManager implements DocumentSymbolProvider { 13 | private symbolSearchManager: SymbolSearchManager; 14 | 15 | public constructor(context: ExtensionContext, configuration: Configuration) { 16 | this.symbolSearchManager = new SymbolSearchManager(configuration); 17 | context.subscriptions.push( 18 | languages.registerDocumentSymbolProvider(getDocumentFilter(), this) 19 | ); 20 | } 21 | 22 | public provideDocumentSymbols(document: TextDocument): Thenable { 23 | return this.symbolSearchManager.findSymbolsInDocument(document.fileName); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/symbol_provision/symbol_information_parser.ts: -------------------------------------------------------------------------------- 1 | import { Position, Range, SymbolInformation, SymbolKind, Uri } from 'vscode'; 2 | 3 | interface RustSymbol { 4 | path: string; 5 | name: string; 6 | container: string; 7 | kind: string; 8 | line: number; 9 | } 10 | 11 | export class SymbolInformationParser { 12 | private kinds: { [key: string]: SymbolKind }; 13 | 14 | public constructor() { 15 | this.kinds = { 16 | 'struct': SymbolKind.Class, 17 | 'method': SymbolKind.Method, 18 | 'field': SymbolKind.Field, 19 | 'function': SymbolKind.Function, 20 | 'constant': SymbolKind.Constant, 21 | 'static': SymbolKind.Constant, 22 | 'enum': SymbolKind.Enum, 23 | // Don't really like this, 24 | // but this was the best alternative given the absense of SymbolKind.Macro 25 | 'macro': SymbolKind.Function 26 | }; 27 | } 28 | 29 | public parseJson(json: string): SymbolInformation[] { 30 | const rustSymbols: RustSymbol[] = JSON.parse(json); 31 | 32 | const symbolInformationList: (SymbolInformation | undefined)[] = rustSymbols.map(rustSymbol => { 33 | const kind = this.getSymbolKind(rustSymbol.kind); 34 | 35 | if (!kind) { 36 | return undefined; 37 | } 38 | 39 | const pos = new Position(rustSymbol.line - 1, 0); 40 | 41 | const range = new Range(pos, pos); 42 | 43 | const uri = Uri.file(rustSymbol.path); 44 | 45 | const symbolInformation = new SymbolInformation( 46 | rustSymbol.name, 47 | kind, 48 | range, 49 | uri, 50 | rustSymbol.container 51 | ); 52 | 53 | return symbolInformation; 54 | }).filter(value => value !== undefined); 55 | 56 | // It is safe to cast because we filtered out `undefined` values 57 | return symbolInformationList; 58 | } 59 | 60 | private getSymbolKind(kind: string): SymbolKind | undefined { 61 | if (kind === '') { 62 | return undefined; 63 | } else { 64 | return this.kinds[kind]; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/symbol_provision/symbol_search_manager.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from 'child_process'; 2 | import { SymbolInformation, window } from 'vscode'; 3 | import { Configuration } from '../configuration/Configuration'; 4 | import { SymbolInformationParser } from './symbol_information_parser'; 5 | 6 | export class SymbolSearchManager { 7 | private configuration: Configuration; 8 | private symbolInformationParser: SymbolInformationParser; 9 | 10 | public constructor(configuration: Configuration) { 11 | this.configuration = configuration; 12 | this.symbolInformationParser = new SymbolInformationParser(); 13 | } 14 | 15 | public findSymbolsInDocument(documentFilePath: string): Promise { 16 | return this.findSymbols(['search', '-l', documentFilePath]); 17 | } 18 | 19 | public findSymbolsInWorkspace( 20 | workspaceDirPath: string, 21 | query: string 22 | ): Promise { 23 | return this.findSymbols(['search', '-g', workspaceDirPath, query]); 24 | } 25 | 26 | private findSymbols(args: string[]): Promise { 27 | const executable = this.configuration.getRustsymPath(); 28 | const options = { maxBuffer: 1024 * 1024 }; 29 | return new Promise((resolve, reject) => { 30 | execFile(executable, args, options, (err, stdout) => { 31 | try { 32 | if (err && (err).code === 'ENOENT') { 33 | window.showInformationMessage('The "rustsym" command is not available. Make sure it is installed.'); 34 | return resolve([]); 35 | } 36 | const result = stdout.toString(); 37 | const symbols = this.symbolInformationParser.parseJson(result); 38 | return resolve(symbols); 39 | } catch (e) { 40 | reject(e); 41 | } 42 | }); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/symbol_provision/workspace_symbol_provision_manager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExtensionContext, 3 | SymbolInformation, 4 | WorkspaceSymbolProvider, 5 | languages, 6 | window 7 | } from 'vscode'; 8 | import { Configuration } from '../configuration/Configuration'; 9 | import { CurrentWorkingDirectoryManager } from '../configuration/current_working_directory_manager'; 10 | import { SymbolSearchManager } from './symbol_search_manager'; 11 | 12 | export class WorkspaceSymbolProvisionManager implements WorkspaceSymbolProvider { 13 | private configuration: Configuration; 14 | private currentWorkingDirectoryManager: CurrentWorkingDirectoryManager; 15 | private symbolSearchManager: SymbolSearchManager; 16 | 17 | public constructor( 18 | context: ExtensionContext, 19 | configuration: Configuration, 20 | currentWorkingDirectoryManager: CurrentWorkingDirectoryManager 21 | ) { 22 | this.configuration = configuration; 23 | this.currentWorkingDirectoryManager = currentWorkingDirectoryManager; 24 | this.symbolSearchManager = new SymbolSearchManager(configuration); 25 | context.subscriptions.push(languages.registerWorkspaceSymbolProvider(this)); 26 | } 27 | 28 | public provideWorkspaceSymbols(query: string): Thenable { 29 | return new Promise((resolve, reject) => { 30 | const cwdPromise = this.currentWorkingDirectoryManager.cwd(); 31 | cwdPromise.then((workspaceDirPath: string) => { 32 | const symbolInformationListPromise = 33 | this.symbolSearchManager.findSymbolsInWorkspace(workspaceDirPath, query); 34 | symbolInformationListPromise.then((symbolInformationList) => { 35 | resolve(symbolInformationList); 36 | }); 37 | }).catch((error: Error) => { 38 | window.showErrorMessage(error.message); 39 | reject(error.message); 40 | }); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/tools_installation/installator.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import * as path from 'path'; 3 | import { ExtensionContext, commands, window } from 'vscode'; 4 | import { CargoInvocationManager } from '../../CargoInvocationManager'; 5 | import { getCommandForArgs, getCommandToExecuteStatementsOneByOneIfPreviousIsSucceed } 6 | from '../../CommandLine'; 7 | import { ShellProviderManager } from '../../ShellProviderManager'; 8 | import { Configuration } from '../configuration/Configuration'; 9 | import { ChildLogger } from '../logging/child_logger'; 10 | import { MissingToolsStatusBarItem } from './missing_tools_status_bar_item'; 11 | 12 | export class Installator { 13 | private _configuration: Configuration; 14 | private _cargoInvocationManager: CargoInvocationManager; 15 | private _shellProvider: ShellProviderManager; 16 | private _logger: ChildLogger; 17 | private _missingToolsStatusBarItem: MissingToolsStatusBarItem; 18 | private _missingTools: string[]; 19 | 20 | public constructor( 21 | context: ExtensionContext, 22 | configuration: Configuration, 23 | cargoInvocationManager: CargoInvocationManager, 24 | shellProviderManager: ShellProviderManager, 25 | logger: ChildLogger 26 | ) { 27 | this._configuration = configuration; 28 | this._cargoInvocationManager = cargoInvocationManager; 29 | this._shellProvider = shellProviderManager; 30 | this._logger = logger; 31 | const installToolsCommandName = 'rust.install_missing_tools'; 32 | this._missingToolsStatusBarItem = new MissingToolsStatusBarItem(context, installToolsCommandName); 33 | this._missingTools = []; 34 | commands.registerCommand(installToolsCommandName, () => { 35 | this.offerToInstallMissingTools(); 36 | }); 37 | } 38 | 39 | public addStatusBarItemIfSomeToolsAreMissing(): void { 40 | this.getMissingTools(); 41 | if (this._missingTools.length === 0) { 42 | return; 43 | } 44 | this._missingToolsStatusBarItem.show(); 45 | } 46 | 47 | private offerToInstallMissingTools(): void { 48 | // Plurality is important. :') 49 | const group = this._missingTools.length > 1 ? 'them' : 'it'; 50 | const message = `You are missing ${this._missingTools.join(', ')}. Would you like to install ${group}?`; 51 | const option = { title: 'Install' }; 52 | window.showInformationMessage(message, option).then(selection => { 53 | if (selection !== option) { 54 | return; 55 | } 56 | this.installMissingTools(); 57 | }); 58 | } 59 | 60 | private async installMissingTools(): Promise { 61 | const terminal = window.createTerminal('Rust tools installation'); 62 | // cargo install tool && cargo install another_tool 63 | const { executable: cargoExecutable, args: cargoArgs } = this._cargoInvocationManager.getExecutableAndArgs(); 64 | const shell = await this._shellProvider.getValue(); 65 | if (shell === undefined) { 66 | return; 67 | } 68 | const statements = this._missingTools.map(tool => { 69 | const args = [cargoExecutable, ...cargoArgs, 'install', tool]; 70 | return getCommandForArgs(shell, args); 71 | }); 72 | const command = getCommandToExecuteStatementsOneByOneIfPreviousIsSucceed(shell, statements); 73 | terminal.sendText(command); 74 | terminal.show(); 75 | this._missingToolsStatusBarItem.hide(); 76 | } 77 | 78 | private getMissingTools(): void { 79 | const logger = this._logger.createChildLogger('getMissingTools(): '); 80 | const pathDirectories: string[] = (process.env.PATH || '').split(path.delimiter); 81 | logger.debug(`pathDirectories=${JSON.stringify(pathDirectories)}`); 82 | const tools: { [tool: string]: string | undefined } = { 83 | 'racer': this._configuration.getPathToRacer(), 84 | 'rustfmt': this._configuration.getRustfmtPath(), 85 | 'rustsym': this._configuration.getRustsymPath() 86 | }; 87 | logger.debug(`tools=${JSON.stringify(tools)}`); 88 | const keys = Object.keys(tools); 89 | const missingTools = keys.map(tool => { 90 | // Check if the path exists as-is. 91 | let userPath = tools[tool]; 92 | if (!userPath) { 93 | // A path is undefined, so a tool is missing 94 | return tool; 95 | } 96 | if (existsSync(userPath)) { 97 | logger.debug(`${tool}'s path=${userPath}`); 98 | return undefined; 99 | } 100 | // If the extension is running on Windows and no extension was 101 | // specified (likely because the user didn't configure a custom path), 102 | // then prefix one for them. 103 | if (process.platform === 'win32' && path.extname(userPath).length === 0) { 104 | userPath += '.exe'; 105 | } 106 | // Check if the tool exists on the PATH 107 | for (const part of pathDirectories) { 108 | const binPath = path.join(part, userPath); 109 | if (existsSync(binPath)) { 110 | return undefined; 111 | } 112 | } 113 | // The tool wasn't found, we should install it 114 | return tool; 115 | }).filter(tool => tool !== undefined); 116 | this._missingTools = missingTools; 117 | logger.debug(`this.missingTools = ${JSON.stringify(this._missingTools)}`); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/components/tools_installation/missing_tools_status_bar_item.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, StatusBarAlignment, StatusBarItem, languages, window } from 'vscode'; 2 | import { getDocumentFilter } from '../configuration/mod'; 3 | 4 | export class MissingToolsStatusBarItem { 5 | private statusBarItem: StatusBarItem; 6 | private canBeShown: boolean; 7 | 8 | public constructor(context: ExtensionContext, statusBarItemCommand: string) { 9 | this.statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right); 10 | this.statusBarItem.color = 'yellow'; 11 | this.statusBarItem.command = statusBarItemCommand; 12 | this.statusBarItem.text = 'Rust Tools Missing'; 13 | this.statusBarItem.tooltip = 'Missing Rust tools'; 14 | this.canBeShown = false; 15 | context.subscriptions.push( 16 | window.onDidChangeActiveTextEditor(() => { 17 | this.updateStatusBarItemVisibility(); 18 | }) 19 | ); 20 | } 21 | 22 | public show(): void { 23 | this.statusBarItem.show(); 24 | this.canBeShown = true; 25 | } 26 | 27 | public hide(): void { 28 | this.statusBarItem.hide(); 29 | this.canBeShown = false; 30 | } 31 | 32 | public updateStatusBarItemVisibility(): void { 33 | if (!this.canBeShown) { 34 | return; 35 | } 36 | if (!window.activeTextEditor) { 37 | this.statusBarItem.hide(); 38 | return; 39 | } 40 | if (languages.match(getDocumentFilter(), window.activeTextEditor.document)) { 41 | this.statusBarItem.show(); 42 | } else { 43 | this.statusBarItem.hide(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/tools_installation/mod.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/editor-rs/vscode-rust/f78d23430af4a6429f7b686e6c5f7c3895e2052a/src/components/tools_installation/mod.ts -------------------------------------------------------------------------------- /src/legacy_mode_manager.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext } from 'vscode'; 2 | import { Configuration } from './components/configuration/Configuration'; 3 | import { RustSource } from './components/configuration/RustSource'; 4 | import { Rustup } from './components/configuration/Rustup'; 5 | import { CurrentWorkingDirectoryManager } 6 | from './components/configuration/current_working_directory_manager'; 7 | import { CompletionManager } from './components/completion/completion_manager'; 8 | import { FormattingManager } from './components/formatting/formatting_manager'; 9 | import { ChildLogger } from './components/logging/child_logger'; 10 | import { DocumentSymbolProvisionManager } 11 | from './components/symbol_provision/document_symbol_provision_manager'; 12 | import { WorkspaceSymbolProvisionManager } 13 | from './components/symbol_provision/workspace_symbol_provision_manager'; 14 | import { Installator as MissingToolsInstallator } 15 | from './components/tools_installation/installator'; 16 | import { CargoInvocationManager } from './CargoInvocationManager'; 17 | import { ShellProviderManager } from './ShellProviderManager'; 18 | 19 | export class LegacyModeManager { 20 | private context: ExtensionContext; 21 | private configuration: Configuration; 22 | private completionManager: CompletionManager; 23 | private formattingManager: FormattingManager | undefined; 24 | private workspaceSymbolProvisionManager: WorkspaceSymbolProvisionManager; 25 | private documentSymbolProvisionManager: DocumentSymbolProvisionManager; 26 | private missingToolsInstallator: MissingToolsInstallator; 27 | 28 | public static async create( 29 | context: ExtensionContext, 30 | configuration: Configuration, 31 | cargoInvocationManager: CargoInvocationManager, 32 | rustSource: RustSource, 33 | rustup: Rustup | undefined, 34 | currentWorkingDirectoryManager: CurrentWorkingDirectoryManager, 35 | shellProviderManager: ShellProviderManager, 36 | logger: ChildLogger 37 | ): Promise { 38 | const formattingManager: FormattingManager | undefined = await FormattingManager.create(context, configuration, logger); 39 | return new LegacyModeManager( 40 | context, 41 | configuration, 42 | cargoInvocationManager, 43 | rustSource, 44 | rustup, 45 | currentWorkingDirectoryManager, 46 | shellProviderManager, 47 | logger, 48 | formattingManager 49 | ); 50 | } 51 | 52 | public async start(): Promise { 53 | this.context.subscriptions.push(this.completionManager.disposable()); 54 | await this.configuration.updatePathToRacer(); 55 | await this.missingToolsInstallator.addStatusBarItemIfSomeToolsAreMissing(); 56 | await this.completionManager.initialStart(); 57 | } 58 | 59 | private constructor( 60 | context: ExtensionContext, 61 | configuration: Configuration, 62 | cargoInvocationManager: CargoInvocationManager, 63 | rustSource: RustSource, 64 | rustup: Rustup | undefined, 65 | currentWorkingDirectoryManager: CurrentWorkingDirectoryManager, 66 | shellProviderManager: ShellProviderManager, 67 | logger: ChildLogger, 68 | formattingManager: FormattingManager | undefined 69 | ) { 70 | this.context = context; 71 | this.configuration = configuration; 72 | this.completionManager = new CompletionManager( 73 | context, 74 | configuration, 75 | rustSource, 76 | rustup, 77 | logger.createChildLogger('CompletionManager: ') 78 | ); 79 | this.formattingManager = formattingManager; 80 | this.workspaceSymbolProvisionManager = new WorkspaceSymbolProvisionManager( 81 | context, 82 | configuration, 83 | currentWorkingDirectoryManager 84 | ); 85 | this.documentSymbolProvisionManager = new DocumentSymbolProvisionManager(context, configuration); 86 | this.missingToolsInstallator = new MissingToolsInstallator( 87 | context, 88 | configuration, 89 | cargoInvocationManager, 90 | shellProviderManager, 91 | logger.createChildLogger('MissingToolsInstallator: ') 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/CommandLine.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { escapeSpaces } from '../src/CommandLine'; 4 | import { Shell } from '../src/Shell'; 5 | 6 | suite('CommandLine tests', () => { 7 | suite('escape_spaces', () => { 8 | test('does not escape a string if the string has no spaces', () => { 9 | assert.equal(escapeSpaces('/home/user', Shell.Shell), '/home/user'); 10 | assert.equal(escapeSpaces('/home/user', Shell.Wsl), '/home/user'); 11 | assert.equal(escapeSpaces('C:\\User', Shell.Cmd), 'C:\\User'); 12 | assert.equal(escapeSpaces('C:\\User', Shell.PowerShell), 'C:\\User'); 13 | }); 14 | test('escapes spaces', () => { 15 | assert.equal(escapeSpaces('/home/some user with spaces', Shell.Shell), '\'/home/some user with spaces\''); 16 | assert.equal(escapeSpaces('/home/some user with spaces', Shell.Wsl), '\'/home/some user with spaces\''); 17 | assert.equal(escapeSpaces('C:\\Some user with spaces', Shell.PowerShell), 'C:\\Some` user` with` spaces'); 18 | assert.equal(escapeSpaces('C:\\Some user with spaces', Shell.Cmd), '"C:\\Some user with spaces"'); 19 | }); 20 | test('does not escape escaped spaces', () => { 21 | assert.equal(escapeSpaces('\'/home/some user with spaces\'', Shell.Shell), '\'/home/some user with spaces\''); 22 | assert.equal(escapeSpaces('\'/home/some user with spaces\'', Shell.Wsl), '\'/home/some user with spaces\''); 23 | assert.equal(escapeSpaces('C:\\Some` user` with` spaces', Shell.PowerShell), 'C:\\Some` user` with` spaces'); 24 | assert.equal(escapeSpaces('"C:\\Some user with spaces"', Shell.Cmd), '"C:\\Some user with spaces"'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/OutputtingProcess.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { OutputtingProcess } from '../src/OutputtingProcess'; 4 | 5 | suite('OutputtingProcess tests', function (): void { 6 | test('output contains data from stdout', function (this, done): void { 7 | const args = ['-e', 'console.log("data")']; 8 | 9 | OutputtingProcess.spawn('node', args).then(function (output): void { 10 | try { 11 | assert.equal(output.success, true); 12 | 13 | if (output.success) { 14 | assert.equal(output.exitCode, 0); 15 | 16 | assert.equal(output.stderrData, ''); 17 | 18 | assert.equal(output.stdoutData, 'data\n'); 19 | 20 | done(); 21 | } 22 | } catch (e) { 23 | done(e); 24 | } 25 | }); 26 | }); 27 | 28 | test('output contains data from stderr', function (this, done): void { 29 | const args = ['-e', 'console.error("data")']; 30 | 31 | OutputtingProcess.spawn('node', args).then(function (output): void { 32 | try { 33 | assert.equal(output.success, true); 34 | 35 | if (output.success) { 36 | assert.equal(output.exitCode, 0); 37 | 38 | assert.equal(output.stderrData, 'data\n'); 39 | 40 | assert.equal(output.stdoutData, ''); 41 | 42 | done(); 43 | } 44 | } catch (e) { 45 | done(e); 46 | } 47 | }); 48 | }); 49 | 50 | test('output contains exit code', function (this, done): void { 51 | const args = ['-e', 'process.exit(1)']; 52 | 53 | OutputtingProcess.spawn('node', args).then(function (output): void { 54 | try { 55 | assert.equal(output.success, true); 56 | 57 | if (output.success) { 58 | assert.equal(output.exitCode, 1); 59 | 60 | assert.equal(output.stderrData, ''); 61 | 62 | assert.equal(output.stdoutData, ''); 63 | 64 | done(); 65 | } 66 | } catch (e) { 67 | done(e); 68 | } 69 | }); 70 | }); 71 | 72 | test('handles not existing executable', function (this, done): void { 73 | OutputtingProcess.spawn('nnnnnnnode').then(function (output): void { 74 | try { 75 | assert.equal(output.success, false); 76 | 77 | if (!output.success) { 78 | assert.equal(output.error, 'ENOENT'); 79 | 80 | done(); 81 | } 82 | } catch (e) { 83 | done(e); 84 | } 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/Shell.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { VALUES, VALUE_STRINGS, fromString } from '../src/Shell'; 3 | 4 | suite('Shell tests', () => { 5 | suite('fromString', () => { 6 | test('parses all possible values to expected values', () => { 7 | assert.deepEqual(VALUE_STRINGS.map(fromString), VALUES); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/WslShellUtils.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { correctPath } from '../src/WslShellUtils'; 3 | 4 | suite('WslShellUtils tests', () => { 5 | suite('correctPath', () => { 6 | test('works', () => { 7 | assert.equal(correctPath('C:\\Directory'), '/mnt/c/Directory'); 8 | assert.equal(correctPath('E:\\Some\\Directory'), '/mnt/e/Some/Directory'); 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/components/cargo/diagnostic_utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { Diagnostic, Range, Uri, languages } from 'vscode'; 4 | 5 | import { addUniqueDiagnostic, isUniqueDiagnostic, normalizeDiagnosticPath } from '../../../src/components/cargo/diagnostic_utils'; 6 | 7 | import { FileDiagnostic } from '../../../src/components/cargo/file_diagnostic'; 8 | 9 | suite('Diagnostic Utils Tests', () => { 10 | suite('normalizeDiagnosticPath', () => { 11 | test('It works for a relative path', () => { 12 | if (process.platform === 'win32') { 13 | assert.equal(normalizeDiagnosticPath('src\\main.rs', 'C:\\Project'), 'C:\\Project\\src\\main.rs'); 14 | } else { 15 | assert.equal(normalizeDiagnosticPath('src/main.rs', '/project'), '/project/src/main.rs'); 16 | } 17 | }); 18 | 19 | test('It works for an absolute path', () => { 20 | if (process.platform === 'win32') { 21 | assert.equal(normalizeDiagnosticPath('C:\\Library\\src\\lib.rs', 'C:\\Project'), 'C:\\Library\\src\\lib.rs'); 22 | } else { 23 | assert.equal(normalizeDiagnosticPath('/library/src/lib.rs', '/project'), '/library/src/lib.rs'); 24 | } 25 | }); 26 | }); 27 | 28 | suite('isUniqueDiagnostic', () => { 29 | test('It returns true for empty diagnostics', () => { 30 | const result = isUniqueDiagnostic(new Diagnostic(new Range(0, 0, 0, 0), '', undefined), []); 31 | 32 | assert.equal(result, true); 33 | }); 34 | 35 | test('It returns true is the diagnostics do not contain any similar diagnostic', () => { 36 | const diagnostics = [ 37 | new Diagnostic(new Range(0, 0, 0, 0), '', undefined) 38 | ]; 39 | 40 | const result = isUniqueDiagnostic(new Diagnostic(new Range(1, 2, 3, 4), 'Hello', undefined), diagnostics); 41 | 42 | assert.equal(result, true); 43 | }); 44 | 45 | test('It returns true is the diagnostics contain a diagnostic with same range, but different message', () => { 46 | const diagnostics = [ 47 | new Diagnostic(new Range(0, 0, 0, 0), '', undefined) 48 | ]; 49 | 50 | const result = isUniqueDiagnostic(new Diagnostic(new Range(0, 0, 0, 0), 'Hello', undefined), diagnostics); 51 | 52 | assert.equal(result, true); 53 | }); 54 | 55 | test('It returns true is the diagnostics contain a diagnostic with same message, but different range', () => { 56 | const diagnostics = [ 57 | new Diagnostic(new Range(0, 0, 0, 0), 'Hello', undefined) 58 | ]; 59 | 60 | const result = isUniqueDiagnostic(new Diagnostic(new Range(1, 2, 3, 4), 'Hello', undefined), diagnostics); 61 | 62 | assert.equal(result, true); 63 | }); 64 | 65 | test('It returns false is the diagnostics contain a diagnostic with the same message and range', () => { 66 | const diagnostics = [ 67 | new Diagnostic(new Range(1, 2, 3, 4), 'Hello', undefined) 68 | ]; 69 | 70 | const result = isUniqueDiagnostic(new Diagnostic(new Range(1, 2, 3, 4), 'Hello', undefined), diagnostics); 71 | 72 | assert.equal(result, false); 73 | }); 74 | }); 75 | 76 | test('addUniqueDiagnostic adds the diagnostic to the empty diagnostics', () => { 77 | const diagnostic: FileDiagnostic = { 78 | filePath: '/1', 79 | diagnostic: new Diagnostic(new Range(1, 2, 3, 4), 'Hello', undefined) 80 | }; 81 | 82 | const diagnostics = languages.createDiagnosticCollection('rust'); 83 | 84 | addUniqueDiagnostic(diagnostic, diagnostics); 85 | 86 | const fileDiagnostics = diagnostics.get(Uri.file('/1')); 87 | 88 | if (!fileDiagnostics) { 89 | assert.notEqual(fileDiagnostics, undefined); 90 | } else { 91 | assert.equal(fileDiagnostics.length, 1); 92 | } 93 | }); 94 | 95 | suite('addUniqueDiagnostic', () => { 96 | test('It adds the diagnostic to the diagnostics which do not contain any similar diagnostic', () => { 97 | const diagnostic: FileDiagnostic = { 98 | filePath: '/1', 99 | diagnostic: new Diagnostic(new Range(1, 2, 3, 4), 'Hello', undefined) 100 | }; 101 | 102 | const diagnostics = languages.createDiagnosticCollection('rust'); 103 | diagnostics.set(Uri.file('/1'), [ 104 | new Diagnostic(new Range(2, 3, 3, 4), 'Hello', undefined), 105 | new Diagnostic(new Range(1, 2, 3, 4), 'Hell', undefined) 106 | ]); 107 | 108 | addUniqueDiagnostic(diagnostic, diagnostics); 109 | 110 | const fileDiagnostics = diagnostics.get(Uri.file('/1')); 111 | 112 | if (!fileDiagnostics) { 113 | assert.notEqual(fileDiagnostics, undefined); 114 | } else { 115 | assert.equal(fileDiagnostics.length, 3); 116 | } 117 | }); 118 | 119 | test('It does not add the diagnostic to the diagnostics which contain any similar diagnostic', () => { 120 | const diagnostic: FileDiagnostic = { 121 | filePath: '/1', 122 | diagnostic: new Diagnostic(new Range(1, 2, 3, 4), 'Hello', undefined) 123 | }; 124 | 125 | const diagnostics = languages.createDiagnosticCollection('rust'); 126 | diagnostics.set(Uri.file('/1'), [ 127 | new Diagnostic(new Range(1, 2, 3, 4), 'Hello', undefined), 128 | new Diagnostic(new Range(1, 2, 3, 4), 'Hell', undefined) 129 | ]); 130 | 131 | addUniqueDiagnostic(diagnostic, diagnostics); 132 | 133 | const fileDiagnostics = diagnostics.get(Uri.file('/1')); 134 | 135 | if (!fileDiagnostics) { 136 | assert.notEqual(fileDiagnostics, undefined); 137 | } else { 138 | assert.equal(fileDiagnostics.length, 2); 139 | } 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // import testRunner = require('vscode/lib/testrunner'); 16 | 17 | // You can directly control Mocha options by uncommenting the following lines 18 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 19 | testRunner.configure({ 20 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 21 | useColors: true // colored output from test results 22 | }); 23 | 24 | module.exports = testRunner; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["es6"], 7 | "sourceMap": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "noEmitOnError": true, 10 | "noImplicitReturns": true, 11 | "noImplicitAny": true, 12 | "noImplicitThis": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "strictNullChecks": true 16 | }, 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "ban": false, 4 | "class-name": true, 5 | "comment-format": [ 6 | true, 7 | "check-space" 8 | ], 9 | "curly": true, 10 | "eofline": true, 11 | "linebreak-style": [ 12 | true, 13 | "LF" 14 | ], 15 | "forin": true, 16 | "indent": [ 17 | true, 18 | "spaces" 19 | ], 20 | "interface-name": false, 21 | "jsdoc-format": true, 22 | "label-position": true, 23 | "member-access": true, 24 | "member-ordering": [ 25 | true, 26 | { 27 | "order": [ 28 | "static-field", 29 | "instance-field", 30 | "public-static-method", 31 | "public-constructor", 32 | "public-instance-method", 33 | "protected-static-method", 34 | "protected-constructor", 35 | "protected-instance-method", 36 | "private-static-method", 37 | "private-constructor", 38 | "private-instance-method" 39 | ] 40 | } 41 | ], 42 | "prefer-const": [ 43 | true, 44 | { 45 | "destructuring": "all" 46 | } 47 | ], 48 | "no-any": false, 49 | "no-arg": true, 50 | "no-bitwise": false, 51 | "no-conditional-assignment": true, 52 | "no-consecutive-blank-lines": false, 53 | "no-console": true, 54 | "no-construct": true, 55 | "no-debugger": true, 56 | "no-duplicate-variable": true, 57 | "no-empty": true, 58 | "no-eval": true, 59 | "no-inferrable-types": true, 60 | "no-internal-module": true, 61 | "no-require-imports": false, 62 | "no-shadowed-variable": true, 63 | "no-string-literal": false, 64 | "no-switch-case-fall-through": true, 65 | "no-trailing-whitespace": true, 66 | "no-unused-expression": true, 67 | "no-var-keyword": true, 68 | "no-var-requires": true, 69 | "object-literal-sort-keys": false, 70 | "one-line": [ 71 | true, 72 | "check-open-brace", 73 | "check-catch", 74 | "check-else", 75 | "check-whitespace" 76 | ], 77 | "quotemark": [ 78 | true, 79 | "single", 80 | "avoid-escape" 81 | ], 82 | "radix": true, 83 | "semicolon": true, 84 | "trailing-comma": [ 85 | true, 86 | { 87 | "multiline": "never", 88 | "singleline": "never" 89 | } 90 | ], 91 | "triple-equals": [ 92 | true, 93 | "allow-null-check" 94 | ], 95 | "typedef": [ 96 | true, 97 | "call-signature", 98 | "property-declaration", 99 | "member-variable-declaration" 100 | ], 101 | "typedef-whitespace": [ 102 | true, 103 | { 104 | "call-signature": "nospace", 105 | "index-signature": "nospace", 106 | "parameter": "nospace", 107 | "property-declaration": "nospace", 108 | "variable-declaration": "nospace" 109 | } 110 | ], 111 | "variable-name": [ 112 | true, 113 | "check-format", 114 | "allow-leading-underscore", 115 | "ban-keywords" 116 | ], 117 | "whitespace": [ 118 | true, 119 | "check-branch", 120 | "check-decl", 121 | "check-operator", 122 | "check-separator", 123 | "check-type" 124 | ] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /typings/elegant-spinner/elegant-spinner.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'elegant-spinner' { 2 | function spinner(): () => string; 3 | export = spinner; 4 | } 5 | -------------------------------------------------------------------------------- /typings/expand-tilde/expand-tilde.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'expand-tilde' { 2 | function expandTilde(path: string): string; 3 | 4 | export = expandTilde; 5 | } 6 | -------------------------------------------------------------------------------- /typings/find-up/find-up.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'find-up' { 2 | function findUp(filename: string, options?: any): Promise; 3 | export = findUp; 4 | } 5 | -------------------------------------------------------------------------------- /typings/tmp/tmp.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for tmp v0.0.28 2 | // Project: https://www.npmjs.com/package/tmp 3 | // Definitions by: Jared Klopper 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare module "tmp" { 7 | 8 | module tmp { 9 | interface Options extends SimpleOptions { 10 | mode?: number; 11 | } 12 | 13 | interface SimpleOptions { 14 | prefix?: string; 15 | postfix?: string; 16 | template?: string; 17 | dir?: string; 18 | tries?: number; 19 | keep?: boolean; 20 | unsafeCleanup?: boolean; 21 | } 22 | 23 | interface SynchrounousResult { 24 | name: string; 25 | fd: number; 26 | removeCallback: () => void; 27 | } 28 | 29 | function file(callback: (err: any, path: string, fd: number, cleanupCallback: () => void) => void): void; 30 | function file(config: Options, callback?: (err: any, path: string, fd: number, cleanupCallback: () => void) => void): void; 31 | 32 | function fileSync(config?: Options): SynchrounousResult; 33 | 34 | function dir(callback: (err: any, path: string, cleanupCallback: () => void) => void): void; 35 | function dir(config: Options, callback?: (err: any, path: string, cleanupCallback: () => void) => void): void; 36 | 37 | function dirSync(config?: Options): SynchrounousResult; 38 | 39 | function tmpName(callback: (err: any, path: string) => void): void; 40 | function tmpName(config: SimpleOptions, callback?: (err: any, path: string) => void): void; 41 | 42 | function tmpNameSync(config?: SimpleOptions): string; 43 | 44 | function setGracefulCleanup(): void; 45 | } 46 | 47 | export = tmp; 48 | } 49 | -------------------------------------------------------------------------------- /typings/tree-kill/tree-kill.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'tree-kill' { 2 | function kill(pid: number, signal?: string, callback?: (err: any) => void): void; 3 | export = kill; 4 | } 5 | --------------------------------------------------------------------------------