├── .eslintrc.json ├── .github ├── dependabot.yml └── release.yml ├── .gitignore ├── .prettierrc.js ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── bundled └── tool │ ├── __init__.py │ ├── _debug_server.py │ ├── lsp_jsonrpc.py │ ├── lsp_runner.py │ ├── lsp_server.py │ └── lsp_utils.py ├── noxfile.py ├── package-lock.json ├── package.json ├── requirements.in ├── requirements.txt ├── runtime.txt ├── src ├── common │ ├── constants.ts │ ├── log │ │ └── logging.ts │ ├── python.ts │ ├── server.ts │ ├── settings.ts │ ├── setup.ts │ ├── utilities.ts │ └── vscodeapi.ts ├── extension.ts └── test │ └── python_tests │ ├── __init__.py │ ├── lsp_test_client │ ├── __init__.py │ ├── constants.py │ ├── defaults.py │ ├── session.py │ └── utils.py │ ├── requirements.in │ ├── requirements.txt │ ├── test_data │ └── sample1 │ │ ├── sample.py │ │ └── sample.unformatted │ └── test_server.py ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | labels: 8 | - 'no-changelog' 9 | 10 | - package-ecosystem: 'pip' 11 | directory: /src/test/python_tests 12 | schedule: 13 | interval: daily 14 | labels: 15 | - 'no-changelog' 16 | 17 | - package-ecosystem: 'pip' 18 | directory: / 19 | schedule: 20 | interval: daily 21 | labels: 22 | - 'debt' 23 | commit-message: 24 | include: 'scope' 25 | prefix: 'pip' 26 | 27 | - package-ecosystem: 'npm' 28 | directory: / 29 | schedule: 30 | interval: monthly 31 | labels: 32 | - 'no-changelog' 33 | ignore: 34 | - dependency-name: '@types/vscode' 35 | - dependency-name: '@types/node' 36 | - dependency-name: 'vscode-languageclient' 37 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - 'no-changelog' 5 | 6 | categories: 7 | - title: Enhancements 8 | labels: 9 | - 'feature-request' 10 | 11 | - title: Bug Fixes 12 | labels: 13 | - 'bug' 14 | 15 | - title: Code Health 16 | labels: 17 | - 'debt' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .venv/ 7 | .vs/ 8 | .nox/ 9 | bundled/libs/ 10 | **/__pycache__ 11 | **/.pytest_cache 12 | **/.vs -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | printWidth: 120, 4 | tabWidth: 4, 5 | endOfLine: 'auto', 6 | trailingComma: 'all', 7 | overrides: [ 8 | { 9 | files: ['*.yml', '*.yaml'], 10 | options: { 11 | tabWidth: 2 12 | } 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher", "esbenp.prettier-vscode"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Debug Extension Only", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 14 | "preLaunchTask": "npm: watch", 15 | "presentation": { 16 | "hidden": false, 17 | "group": "", 18 | "order": 2 19 | } 20 | }, 21 | { 22 | "name": "Python Attach", 23 | "type": "python", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}", 26 | "justMyCode": false, 27 | "presentation": { 28 | "hidden": false, 29 | "group": "", 30 | "order": 3 31 | } 32 | }, 33 | { 34 | "name": "Python Config for test explorer (hidden)", 35 | "type": "python", 36 | "request": "launch", 37 | "console": "integratedTerminal", 38 | "purpose": ["debug-test"], 39 | "justMyCode": true, 40 | "presentation": { 41 | "hidden": true, 42 | "group": "", 43 | "order": 4 44 | } 45 | }, 46 | { 47 | "name": "Debug Extension (hidden)", 48 | "type": "extensionHost", 49 | "request": "launch", 50 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 51 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 52 | "env": { 53 | "USE_DEBUGPY": "True" 54 | }, 55 | "presentation": { 56 | "hidden": true, 57 | "group": "", 58 | "order": 4 59 | } 60 | }, 61 | { 62 | "name": "Python debug server (hidden)", 63 | "type": "python", 64 | "request": "attach", 65 | "listen": { "host": "localhost", "port": 5678 }, 66 | "justMyCode": true, 67 | "presentation": { 68 | "hidden": true, 69 | "group": "", 70 | "order": 4 71 | } 72 | } 73 | ], 74 | "compounds": [ 75 | { 76 | "name": "Debug Extension and Python", 77 | "configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"], 78 | "stopAll": true, 79 | "preLaunchTask": "npm: watch", 80 | "presentation": { 81 | "hidden": false, 82 | "group": "", 83 | "order": 1 84 | } 85 | } 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /.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 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off", 13 | "python.testing.pytestArgs": ["src/test/python_tests"], 14 | "python.testing.unittestEnabled": false, 15 | "python.testing.pytestEnabled": true, 16 | "python.testing.cwd": "${workspaceFolder}", 17 | "python.analysis.extraPaths": ["bundled/libs"], 18 | "[typescript]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode", 20 | "editor.formatOnSave": true 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"], 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": ["npm: watch", "npm: watch-tests"], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.ts 13 | .venv/** 14 | .nox/** 15 | .github/ 16 | **/__pycache__/** 17 | **/*.pyc 18 | bundled/libs/bin/** 19 | bundled/libs/*.dist-info/** 20 | noxfile.py 21 | .pytest_cache/** 22 | .pylintrc 23 | **/requirements.txt 24 | **/requirements.in 25 | **/tool/_debug_server.py -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | - Release information can be added here or can be tracked via github release. 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | # TODO: If you want to keep using the MIT license, add your name here. 4 | # To change the license, PREPEND it to this file. 5 | 6 | Copyright (c) Microsoft Corporation. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Template for VS Code python tools extensions 2 | 3 | This is a template repository to get you started on building a VS Code extension for your favorite python tool. It could be a linter, formatter, or code analysis, or all of those together. This template will give you the basic building blocks you need to build a VS Code extension for it. 4 | 5 | ## Programming Languages and Frameworks 6 | 7 | The extension template has two parts, the extension part and language server part. The extension part is written in TypeScript, and language server part is written in Python over the [_pygls_][pygls] (Python language server) library. 8 | 9 | For the most part you will be working on the python part of the code when using this template. You will be integrating your tool with the extension part using the [Language Server Protocol](https://microsoft.github.io/language-server-protocol). [_pygls_][pygls] currently works on the [version 3.16 of LSP](https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/). 10 | 11 | The TypeScript part handles working with VS Code and its UI. The extension template comes with few settings pre configured that can be used by your tool. If you need to add new settings to support your tool, you will have to work with a bit of TypeScript. The extension has examples for few settings that you can follow. You can also look at extensions developed by our team for some of the popular tools as reference. 12 | 13 | ## Requirements 14 | 15 | 1. VS Code 1.64.0 or greater 16 | 1. Python 3.9 or greater 17 | 1. node >= 18.17.0 18 | 1. npm >= 8.19.0 (`npm` is installed with node, check npm version, use `npm install -g npm@8.3.0` to update) 19 | 1. Python extension for VS Code 20 | 21 | You should know to create and work with python virtual environments. 22 | 23 | ## Getting Started 24 | 25 | 1. Use this [template to create your repo](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template). 26 | 1. Check-out your repo locally on your development machine. 27 | 1. Create and activate a python virtual environment for this project in a terminal. Be sure to use the minimum version of python for your tool. This template was written to work with python 3.9 or greater. 28 | 1. Install `nox` in the activated environment: `python -m pip install nox`. 29 | 1. Add your favorite tool to `requirements.in` 30 | 1. Run `nox --session setup`. 31 | 1. **Optional** Install test dependencies `python -m pip install -r src/test/python_tests/requirements.txt`. You will have to install these to run tests from the Test Explorer. 32 | 1. Open `package.json`, look for and update the following things: 33 | 1. Find and replace `` with module name for your tool. This will be used internally to create settings namespace, register commands, etc. Recommendation is to use lower case version of the name, no spaces, `-` are ok. For example, replacing `` with `pylint` will lead to settings looking like `pylint.args`. Another example, replacing `` with `black-formatter` will make settings look like `black-formatter.args`. 34 | 1. Find and replace `` with display name for your tool. This is used as the title for the extension in market place, extensions view, output logs, etc. For example, for the `black` extension this is `Black Formatter`. 35 | 1. Install node packages using `npm install`. 36 | 1. Go to https://marketplace.visualstudio.com/vscode and create a publisher account if you don't already have one. 37 | 1. Use the published name in `package.json` by replacing `` with the name you registered in the marketplace. 38 | 39 | ## Features of this Template 40 | 41 | After finishing the getting started part, this template would have added the following. Assume `` was replaced with `mytool`, and `` with`My Tool`: 42 | 43 | 1. A command `My Tool: Restart Server` (command Id: `mytool.restart`). 44 | 1. Following setting: 45 | - `mytool.args` 46 | - `mytool.path` 47 | - `mytool.importStrategy` 48 | - `mytool.interpreter` 49 | - `mytool.showNotification` 50 | 1. Following triggers for extension activation: 51 | - On Language `python`. 52 | - On File with `.py` extension found in the opened workspace. 53 | 1. Following commands are registered: 54 | - `mytool.restart`: Restarts the language server. 55 | 1. Output Channel for logging `Output` > `My Tool` 56 | 57 | ## Adding features from your tool 58 | 59 | Open `bundled/tool/lsp_server.py`, here is where you will do most of the changes. Look for `TODO` comments there for more details. 60 | 61 | Also look for `TODO` in other locations in the entire template: 62 | 63 | - `bundled/tool/lsp_runner.py` : You may need to update this in some special cases. 64 | - `src/test/python_tests/test_server.py` : This is where you will write tests. There are two incomplete examples provided there to get you started. 65 | - All the markdown files in this template have some `TODO` items, be sure to check them out as well. That includes updating the LICENSE file, even if you want to keep it MIT License. 66 | 67 | References, to other extension created by our team using the template: 68 | 69 | - Protocol reference: 70 | - Implementation showing how to handle Linting on file `open`, `save`, and `close`. [Pylint](https://github.com/microsoft/vscode-pylint/tree/main/bundled/tool) 71 | - Implementation showing how to handle Formatting. [Black Formatter](https://github.com/microsoft/vscode-black-formatter/tree/main/bundled/tool) 72 | - Implementation showing how to handle Code Actions. [isort](https://github.com/microsoft/vscode-isort/blob/main/bundled/tool) 73 | 74 | ## Building and Run the extension 75 | 76 | Run the `Debug Extension and Python` configuration form VS Code. That should build and debug the extension in host window. 77 | 78 | Note: if you just want to build you can run the build task in VS Code (`ctrl`+`shift`+`B`) 79 | 80 | ## Debugging 81 | 82 | To debug both TypeScript and Python code use `Debug Extension and Python` debug config. This is the recommended way. Also, when stopping, be sure to stop both the Typescript, and Python debug sessions. Otherwise, it may not reconnect to the python session. 83 | 84 | To debug only TypeScript code, use `Debug Extension` debug config. 85 | 86 | To debug a already running server or in production server, use `Python Attach`, and select the process that is running `lsp_server.py`. 87 | 88 | ## Logging and Logs 89 | 90 | The template creates a logging Output channel that can be found under `Output` > `mytool` panel. You can control the log level running the `Developer: Set Log Level...` command from the Command Palette, and selecting your extension from the list. It should be listed using the display name for your tool. You can also set the global log level, and that will apply to all extensions and the editor. 91 | 92 | If you need logs that involve messages between the Language Client and Language Server, you can set `"mytool.server.trace": "verbose"`, to get the messaging logs. These logs are also available `Output` > `mytool` panel. 93 | 94 | ## Adding new Settings or Commands 95 | 96 | You can add new settings by adding details for the settings in `package.json` file. To pass this configuration to your python tool server (i.e, `lsp_server.py`) update the `settings.ts` as need. There are examples of different types of settings in that file that you can base your new settings on. 97 | 98 | You can follow how `restart` command is implemented in `package.json` and `extension.ts` for how to add commands. You can also contribute commands from Python via the Language Server Protocol. 99 | 100 | ## Testing 101 | 102 | See `src/test/python_tests/test_server.py` for starting point. See, other referred projects here for testing various aspects of running the tool over LSP. 103 | 104 | If you have installed the test requirements you should be able to see the tests in the test explorer. 105 | 106 | You can also run all tests using `nox --session tests` command. 107 | 108 | ## Linting 109 | 110 | Run `nox --session lint` to run linting on both Python and TypeScript code. Please update the nox file if you want to use a different linter and formatter. 111 | 112 | ## Packaging and Publishing 113 | 114 | 1. Update various fields in `package.json`. At minimum, check the following fields and update them accordingly. See [extension manifest reference](https://code.visualstudio.com/api/references/extension-manifest) to add more fields: 115 | - `"publisher"`: Update this to your publisher id from . 116 | - `"version"`: See for details of requirements and limitations for this field. 117 | - `"license"`: Update license as per your project. Defaults to `MIT`. 118 | - `"keywords"`: Update keywords for your project, these will be used when searching in the VS Code marketplace. 119 | - `"categories"`: Update categories for your project, makes it easier to filter in the VS Code marketplace. 120 | - `"homepage"`, `"repository"`, and `"bugs"` : Update URLs for these fields to point to your project. 121 | - **Optional** Add `"icon"` field with relative path to a image file to use as icon for this project. 122 | 1. Make sure to check the following markdown files: 123 | - **REQUIRED** First time only: `CODE_OF_CONDUCT.md`, `LICENSE`, `SUPPORT.md`, `SECURITY.md` 124 | - Every Release: `CHANGELOG.md` 125 | 1. Build package using `nox --session build_package`. 126 | 1. Take the generated `.vsix` file and upload it to your extension management page . 127 | 128 | To do this from the command line see here 129 | 130 | ## Upgrading Dependencies 131 | 132 | Dependabot yml is provided to make it easy to setup upgrading dependencies in this extension. Be sure to add the labels used in the dependabot to your repo. 133 | 134 | To manually upgrade your local project: 135 | 136 | 1. Create a new branch 137 | 1. Run `npm update` to update node modules. 138 | 1. Run `nox --session setup` to upgrade python packages. 139 | 140 | ## Troubleshooting 141 | 142 | ### Changing path or name of `lsp_server.py` something else 143 | 144 | If you want to change the name of `lsp_server.py` to something else, you can. Be sure to update `constants.ts` and `src/test/python_tests/lsp_test_client/session.py`. 145 | 146 | Also make sure that the inserted paths in `lsp_server.py` are pointing to the right folders to pick up the dependent packages. 147 | 148 | ### Module not found errors 149 | 150 | This can occurs if `bundled/libs` is empty. That is the folder where we put your tool and other dependencies. Be sure to follow the build steps need for creating and bundling the required libs. 151 | 152 | Common one is [_pygls_][pygls] module not found. 153 | 154 | # TODO: The maintainer of this repo has not yet edited this file 155 | 156 | **Repo Owner** Make sure you update this. As a repository owner you will need to update this file with specific instructions for your extension. 157 | 158 | [pygls]: https://github.com/openlawlibrary/pygls 159 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **Repo Owner** Make sure you update this. As a repository owner you will need to decide how you want to handle reporting of security issues for your project. 4 | 5 | --- 6 | 7 | ** Content below this line is the Security information for the template itself ** 8 | 9 | 10 | 11 | ## Security 12 | 13 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 14 | 15 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 16 | 17 | ## Reporting Security Issues 18 | 19 | **Please do not report security vulnerabilities through public GitHub issues.** 20 | 21 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 22 | 23 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 24 | 25 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 26 | 27 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 28 | 29 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 30 | - Full paths of source file(s) related to the manifestation of the issue 31 | - The location of the affected source code (tag/branch/commit or direct URL) 32 | - Any special configuration required to reproduce the issue 33 | - Step-by-step instructions to reproduce the issue 34 | - Proof-of-concept or exploit code (if possible) 35 | - Impact of the issue, including how an attacker might exploit the issue 36 | 37 | This information will help us triage your report more quickly. 38 | 39 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 40 | 41 | ## Preferred Languages 42 | 43 | We prefer all communications to be in English. 44 | 45 | ## Policy 46 | 47 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 48 | 49 | 50 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **Repo Owner** Make sure you update this. As a repository owner you will need to decide how you want to offer support for your extension. 4 | 5 | # Support 6 | 7 | ## How to file issues and get help 8 | 9 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 10 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 11 | feature request as a new Issue. 12 | 13 | For help and questions about using this project, please use the GitHub Discussions. 14 | 15 | ## Microsoft Support Policy 16 | 17 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 18 | -------------------------------------------------------------------------------- /bundled/tool/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /bundled/tool/_debug_server.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """Debugging support for LSP.""" 4 | 5 | import os 6 | import pathlib 7 | import runpy 8 | import sys 9 | 10 | 11 | def update_sys_path(path_to_add: str) -> None: 12 | """Add given path to `sys.path`.""" 13 | if path_to_add not in sys.path and os.path.isdir(path_to_add): 14 | sys.path.append(path_to_add) 15 | 16 | 17 | # Ensure debugger is loaded before we load anything else, to debug initialization. 18 | debugger_path = os.getenv("DEBUGPY_PATH", None) 19 | if debugger_path: 20 | if debugger_path.endswith("debugpy"): 21 | debugger_path = os.fspath(pathlib.Path(debugger_path).parent) 22 | 23 | update_sys_path(debugger_path) 24 | 25 | # pylint: disable=wrong-import-position,import-error 26 | import debugpy 27 | 28 | # 5678 is the default port, If you need to change it update it here 29 | # and in launch.json. 30 | debugpy.connect(5678) 31 | 32 | # This will ensure that execution is paused as soon as the debugger 33 | # connects to VS Code. If you don't want to pause here comment this 34 | # line and set breakpoints as appropriate. 35 | debugpy.breakpoint() 36 | 37 | SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py") 38 | # NOTE: Set breakpoint in `lsp_server.py` before continuing. 39 | runpy.run_path(SERVER_PATH, run_name="__main__") 40 | -------------------------------------------------------------------------------- /bundled/tool/lsp_jsonrpc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """Light-weight JSON-RPC over standard IO.""" 4 | 5 | 6 | import atexit 7 | import contextlib 8 | import io 9 | import json 10 | import pathlib 11 | import subprocess 12 | import threading 13 | import uuid 14 | from concurrent.futures import ThreadPoolExecutor 15 | from typing import BinaryIO, Dict, Optional, Sequence, Union 16 | 17 | CONTENT_LENGTH = "Content-Length: " 18 | RUNNER_SCRIPT = str(pathlib.Path(__file__).parent / "lsp_runner.py") 19 | 20 | 21 | def to_str(text) -> str: 22 | """Convert bytes to string as needed.""" 23 | return text.decode("utf-8") if isinstance(text, bytes) else text 24 | 25 | 26 | class StreamClosedException(Exception): 27 | """JSON RPC stream is closed.""" 28 | 29 | pass # pylint: disable=unnecessary-pass 30 | 31 | 32 | class JsonWriter: 33 | """Manages writing JSON-RPC messages to the writer stream.""" 34 | 35 | def __init__(self, writer: io.TextIOWrapper): 36 | self._writer = writer 37 | self._lock = threading.Lock() 38 | 39 | def close(self): 40 | """Closes the underlying writer stream.""" 41 | with self._lock: 42 | if not self._writer.closed: 43 | self._writer.close() 44 | 45 | def write(self, data): 46 | """Writes given data to stream in JSON-RPC format.""" 47 | if self._writer.closed: 48 | raise StreamClosedException() 49 | 50 | with self._lock: 51 | content = json.dumps(data) 52 | length = len(content.encode("utf-8")) 53 | self._writer.write( 54 | f"{CONTENT_LENGTH}{length}\r\n\r\n{content}".encode("utf-8") 55 | ) 56 | self._writer.flush() 57 | 58 | 59 | class JsonReader: 60 | """Manages reading JSON-RPC messages from stream.""" 61 | 62 | def __init__(self, reader: io.TextIOWrapper): 63 | self._reader = reader 64 | 65 | def close(self): 66 | """Closes the underlying reader stream.""" 67 | if not self._reader.closed: 68 | self._reader.close() 69 | 70 | def read(self): 71 | """Reads data from the stream in JSON-RPC format.""" 72 | if self._reader.closed: 73 | raise StreamClosedException 74 | length = None 75 | while not length: 76 | line = to_str(self._readline()) 77 | if line.startswith(CONTENT_LENGTH): 78 | length = int(line[len(CONTENT_LENGTH) :]) 79 | 80 | line = to_str(self._readline()).strip() 81 | while line: 82 | line = to_str(self._readline()).strip() 83 | 84 | content = to_str(self._reader.read(length)) 85 | return json.loads(content) 86 | 87 | def _readline(self): 88 | line = self._reader.readline() 89 | if not line: 90 | raise EOFError 91 | return line 92 | 93 | 94 | class JsonRpc: 95 | """Manages sending and receiving data over JSON-RPC.""" 96 | 97 | def __init__(self, reader: io.TextIOWrapper, writer: io.TextIOWrapper): 98 | self._reader = JsonReader(reader) 99 | self._writer = JsonWriter(writer) 100 | 101 | def close(self): 102 | """Closes the underlying streams.""" 103 | with contextlib.suppress(Exception): 104 | self._reader.close() 105 | with contextlib.suppress(Exception): 106 | self._writer.close() 107 | 108 | def send_data(self, data): 109 | """Send given data in JSON-RPC format.""" 110 | self._writer.write(data) 111 | 112 | def receive_data(self): 113 | """Receive data in JSON-RPC format.""" 114 | return self._reader.read() 115 | 116 | 117 | def create_json_rpc(readable: BinaryIO, writable: BinaryIO) -> JsonRpc: 118 | """Creates JSON-RPC wrapper for the readable and writable streams.""" 119 | return JsonRpc(readable, writable) 120 | 121 | 122 | class ProcessManager: 123 | """Manages sub-processes launched for running tools.""" 124 | 125 | def __init__(self): 126 | self._args: Dict[str, Sequence[str]] = {} 127 | self._processes: Dict[str, subprocess.Popen] = {} 128 | self._rpc: Dict[str, JsonRpc] = {} 129 | self._lock = threading.Lock() 130 | self._thread_pool = ThreadPoolExecutor(10) 131 | 132 | def stop_all_processes(self): 133 | """Send exit command to all processes and shutdown transport.""" 134 | for i in self._rpc.values(): 135 | with contextlib.suppress(Exception): 136 | i.send_data({"id": str(uuid.uuid4()), "method": "exit"}) 137 | self._thread_pool.shutdown(wait=False) 138 | 139 | def start_process(self, workspace: str, args: Sequence[str], cwd: str) -> None: 140 | """Starts a process and establishes JSON-RPC communication over stdio.""" 141 | # pylint: disable=consider-using-with 142 | proc = subprocess.Popen( 143 | args, 144 | cwd=cwd, 145 | stdout=subprocess.PIPE, 146 | stdin=subprocess.PIPE, 147 | ) 148 | self._processes[workspace] = proc 149 | self._rpc[workspace] = create_json_rpc(proc.stdout, proc.stdin) 150 | 151 | def _monitor_process(): 152 | proc.wait() 153 | with self._lock: 154 | try: 155 | del self._processes[workspace] 156 | rpc = self._rpc.pop(workspace) 157 | rpc.close() 158 | except: # pylint: disable=bare-except 159 | pass 160 | 161 | self._thread_pool.submit(_monitor_process) 162 | 163 | def get_json_rpc(self, workspace: str) -> JsonRpc: 164 | """Gets the JSON-RPC wrapper for the a given id.""" 165 | with self._lock: 166 | if workspace in self._rpc: 167 | return self._rpc[workspace] 168 | raise StreamClosedException() 169 | 170 | 171 | _process_manager = ProcessManager() 172 | atexit.register(_process_manager.stop_all_processes) 173 | 174 | 175 | def _get_json_rpc(workspace: str) -> Union[JsonRpc, None]: 176 | try: 177 | return _process_manager.get_json_rpc(workspace) 178 | except StreamClosedException: 179 | return None 180 | except KeyError: 181 | return None 182 | 183 | 184 | def get_or_start_json_rpc( 185 | workspace: str, interpreter: Sequence[str], cwd: str 186 | ) -> Union[JsonRpc, None]: 187 | """Gets an existing JSON-RPC connection or starts one and return it.""" 188 | res = _get_json_rpc(workspace) 189 | if not res: 190 | args = [*interpreter, RUNNER_SCRIPT] 191 | _process_manager.start_process(workspace, args, cwd) 192 | res = _get_json_rpc(workspace) 193 | return res 194 | 195 | 196 | class RpcRunResult: 197 | """Object to hold result from running tool over RPC.""" 198 | 199 | def __init__(self, stdout: str, stderr: str, exception: Optional[str] = None): 200 | self.stdout: str = stdout 201 | self.stderr: str = stderr 202 | self.exception: Optional[str] = exception 203 | 204 | 205 | # pylint: disable=too-many-arguments 206 | def run_over_json_rpc( 207 | workspace: str, 208 | interpreter: Sequence[str], 209 | module: str, 210 | argv: Sequence[str], 211 | use_stdin: bool, 212 | cwd: str, 213 | source: str = None, 214 | ) -> RpcRunResult: 215 | """Uses JSON-RPC to execute a command.""" 216 | rpc: Union[JsonRpc, None] = get_or_start_json_rpc(workspace, interpreter, cwd) 217 | if not rpc: 218 | raise Exception("Failed to run over JSON-RPC.") 219 | 220 | msg_id = str(uuid.uuid4()) 221 | msg = { 222 | "id": msg_id, 223 | "method": "run", 224 | "module": module, 225 | "argv": argv, 226 | "useStdin": use_stdin, 227 | "cwd": cwd, 228 | } 229 | if source: 230 | msg["source"] = source 231 | 232 | rpc.send_data(msg) 233 | 234 | data = rpc.receive_data() 235 | 236 | if data["id"] != msg_id: 237 | return RpcRunResult( 238 | "", f"Invalid result for request: {json.dumps(msg, indent=4)}" 239 | ) 240 | 241 | result = data["result"] if "result" in data else "" 242 | if "error" in data: 243 | error = data["error"] 244 | 245 | if data.get("exception", False): 246 | return RpcRunResult(result, "", error) 247 | return RpcRunResult(result, error) 248 | 249 | return RpcRunResult(result, "") 250 | 251 | 252 | def shutdown_json_rpc(): 253 | """Shutdown all JSON-RPC processes.""" 254 | _process_manager.stop_all_processes() 255 | -------------------------------------------------------------------------------- /bundled/tool/lsp_runner.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Runner to use when running under a different interpreter. 5 | """ 6 | 7 | import os 8 | import pathlib 9 | import sys 10 | import traceback 11 | 12 | 13 | # ********************************************************** 14 | # Update sys.path before importing any bundled libraries. 15 | # ********************************************************** 16 | def update_sys_path(path_to_add: str, strategy: str) -> None: 17 | """Add given path to `sys.path`.""" 18 | if path_to_add not in sys.path and os.path.isdir(path_to_add): 19 | if strategy == "useBundled": 20 | sys.path.insert(0, path_to_add) 21 | elif strategy == "fromEnvironment": 22 | sys.path.append(path_to_add) 23 | 24 | 25 | # Ensure that we can import LSP libraries, and other bundled libraries. 26 | update_sys_path( 27 | os.fspath(pathlib.Path(__file__).parent.parent / "libs"), 28 | os.getenv("LS_IMPORT_STRATEGY", "useBundled"), 29 | ) 30 | 31 | 32 | # pylint: disable=wrong-import-position,import-error 33 | import lsp_jsonrpc as jsonrpc 34 | import lsp_utils as utils 35 | 36 | RPC = jsonrpc.create_json_rpc(sys.stdin.buffer, sys.stdout.buffer) 37 | 38 | EXIT_NOW = False 39 | while not EXIT_NOW: 40 | msg = RPC.receive_data() 41 | 42 | method = msg["method"] 43 | if method == "exit": 44 | EXIT_NOW = True 45 | continue 46 | 47 | if method == "run": 48 | is_exception = False 49 | # This is needed to preserve sys.path, pylint modifies 50 | # sys.path and that might not work for this scenario 51 | # next time around. 52 | with utils.substitute_attr(sys, "path", sys.path[:]): 53 | try: 54 | # TODO: `utils.run_module` is equivalent to running `python -m `. 55 | # If your tool supports a programmatic API then replace the function below 56 | # with code for your tool. You can also use `utils.run_api` helper, which 57 | # handles changing working directories, managing io streams, etc. 58 | # Also update `_run_tool_on_document` and `_run_tool` functions in `lsp_server.py`. 59 | result = utils.run_module( 60 | module=msg["module"], 61 | argv=msg["argv"], 62 | use_stdin=msg["useStdin"], 63 | cwd=msg["cwd"], 64 | source=msg["source"] if "source" in msg else None, 65 | ) 66 | except Exception: # pylint: disable=broad-except 67 | result = utils.RunResult("", traceback.format_exc(chain=True)) 68 | is_exception = True 69 | 70 | response = {"id": msg["id"]} 71 | if result.stderr: 72 | response["error"] = result.stderr 73 | response["exception"] = is_exception 74 | elif result.stdout: 75 | response["result"] = result.stdout 76 | 77 | RPC.send_data(response) 78 | -------------------------------------------------------------------------------- /bundled/tool/lsp_server.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """Implementation of tool support over LSP.""" 4 | from __future__ import annotations 5 | 6 | import copy 7 | import json 8 | import os 9 | import pathlib 10 | import re 11 | import sys 12 | import sysconfig 13 | import traceback 14 | from typing import Any, Optional, Sequence 15 | 16 | 17 | # ********************************************************** 18 | # Update sys.path before importing any bundled libraries. 19 | # ********************************************************** 20 | def update_sys_path(path_to_add: str, strategy: str) -> None: 21 | """Add given path to `sys.path`.""" 22 | if path_to_add not in sys.path and os.path.isdir(path_to_add): 23 | if strategy == "useBundled": 24 | sys.path.insert(0, path_to_add) 25 | elif strategy == "fromEnvironment": 26 | sys.path.append(path_to_add) 27 | 28 | 29 | # Ensure that we can import LSP libraries, and other bundled libraries. 30 | update_sys_path( 31 | os.fspath(pathlib.Path(__file__).parent.parent / "libs"), 32 | os.getenv("LS_IMPORT_STRATEGY", "useBundled"), 33 | ) 34 | 35 | # ********************************************************** 36 | # Imports needed for the language server goes below this. 37 | # ********************************************************** 38 | # pylint: disable=wrong-import-position,import-error 39 | import lsp_jsonrpc as jsonrpc 40 | import lsp_utils as utils 41 | import lsprotocol.types as lsp 42 | from pygls import server, uris, workspace 43 | 44 | WORKSPACE_SETTINGS = {} 45 | GLOBAL_SETTINGS = {} 46 | RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py" 47 | 48 | MAX_WORKERS = 5 49 | # TODO: Update the language server name and version. 50 | LSP_SERVER = server.LanguageServer( 51 | name="", version="", max_workers=MAX_WORKERS 52 | ) 53 | 54 | 55 | # ********************************************************** 56 | # Tool specific code goes below this. 57 | # ********************************************************** 58 | 59 | # Reference: 60 | # LS Protocol: 61 | # https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/ 62 | # 63 | # Sample implementations: 64 | # Pylint: https://github.com/microsoft/vscode-pylint/blob/main/bundled/tool 65 | # Black: https://github.com/microsoft/vscode-black-formatter/blob/main/bundled/tool 66 | # isort: https://github.com/microsoft/vscode-isort/blob/main/bundled/tool 67 | 68 | # TODO: Update TOOL_MODULE with the module name for your tool. 69 | # e.g, TOOL_MODULE = "pylint" 70 | TOOL_MODULE = "" 71 | 72 | # TODO: Update TOOL_DISPLAY with a display name for your tool. 73 | # e.g, TOOL_DISPLAY = "Pylint" 74 | TOOL_DISPLAY = "" 75 | 76 | # TODO: Update TOOL_ARGS with default argument you have to pass to your tool in 77 | # all scenarios. 78 | TOOL_ARGS = [] # default arguments always passed to your tool. 79 | 80 | 81 | # TODO: If your tool is a linter then update this section. 82 | # Delete "Linting features" section if your tool is NOT a linter. 83 | # ********************************************************** 84 | # Linting features start here 85 | # ********************************************************** 86 | 87 | # See `pylint` implementation for a full featured linter extension: 88 | # Pylint: https://github.com/microsoft/vscode-pylint/blob/main/bundled/tool 89 | 90 | 91 | @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_OPEN) 92 | def did_open(params: lsp.DidOpenTextDocumentParams) -> None: 93 | """LSP handler for textDocument/didOpen request.""" 94 | document = LSP_SERVER.workspace.get_document(params.text_document.uri) 95 | diagnostics: list[lsp.Diagnostic] = _linting_helper(document) 96 | LSP_SERVER.publish_diagnostics(document.uri, diagnostics) 97 | 98 | 99 | @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE) 100 | def did_save(params: lsp.DidSaveTextDocumentParams) -> None: 101 | """LSP handler for textDocument/didSave request.""" 102 | document = LSP_SERVER.workspace.get_document(params.text_document.uri) 103 | diagnostics: list[lsp.Diagnostic] = _linting_helper(document) 104 | LSP_SERVER.publish_diagnostics(document.uri, diagnostics) 105 | 106 | 107 | @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE) 108 | def did_close(params: lsp.DidCloseTextDocumentParams) -> None: 109 | """LSP handler for textDocument/didClose request.""" 110 | document = LSP_SERVER.workspace.get_document(params.text_document.uri) 111 | # Publishing empty diagnostics to clear the entries for this file. 112 | LSP_SERVER.publish_diagnostics(document.uri, []) 113 | 114 | 115 | def _linting_helper(document: workspace.Document) -> list[lsp.Diagnostic]: 116 | # TODO: Determine if your tool supports passing file content via stdin. 117 | # If you want to support linting on change then your tool will need to 118 | # support linting over stdin to be effective. Read, and update 119 | # _run_tool_on_document and _run_tool functions as needed for your project. 120 | result = _run_tool_on_document(document) 121 | return _parse_output_using_regex(result.stdout) if result.stdout else [] 122 | 123 | 124 | # TODO: If your linter outputs in a known format like JSON, then parse 125 | # accordingly. But incase you need to parse the output using RegEx here 126 | # is a helper you can work with. 127 | # flake8 example: 128 | # If you use following format argument with flake8 you can use the regex below to parse it. 129 | # TOOL_ARGS += ["--format='%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s'"] 130 | # DIAGNOSTIC_RE = 131 | # r"(?P\d+),(?P-?\d+),(?P\w+),(?P\w+\d+):(?P[^\r\n]*)" 132 | DIAGNOSTIC_RE = re.compile(r"") 133 | 134 | 135 | def _parse_output_using_regex(content: str) -> list[lsp.Diagnostic]: 136 | lines: list[str] = content.splitlines() 137 | diagnostics: list[lsp.Diagnostic] = [] 138 | 139 | # TODO: Determine if your linter reports line numbers starting at 1 (True) or 0 (False). 140 | line_at_1 = True 141 | # TODO: Determine if your linter reports column numbers starting at 1 (True) or 0 (False). 142 | column_at_1 = True 143 | 144 | line_offset = 1 if line_at_1 else 0 145 | col_offset = 1 if column_at_1 else 0 146 | for line in lines: 147 | if line.startswith("'") and line.endswith("'"): 148 | line = line[1:-1] 149 | match = DIAGNOSTIC_RE.match(line) 150 | if match: 151 | data = match.groupdict() 152 | position = lsp.Position( 153 | line=max([int(data["line"]) - line_offset, 0]), 154 | character=int(data["column"]) - col_offset, 155 | ) 156 | diagnostic = lsp.Diagnostic( 157 | range=lsp.Range( 158 | start=position, 159 | end=position, 160 | ), 161 | message=data.get("message"), 162 | severity=_get_severity(data["code"], data["type"]), 163 | code=data["code"], 164 | source=TOOL_MODULE, 165 | ) 166 | diagnostics.append(diagnostic) 167 | 168 | return diagnostics 169 | 170 | 171 | # TODO: if you want to handle setting specific severity for your linter 172 | # in a user configurable way, then look at look at how it is implemented 173 | # for `pylint` extension from our team. 174 | # Pylint: https://github.com/microsoft/vscode-pylint 175 | # Follow the flow of severity from the settings in package.json to the server. 176 | def _get_severity(*_codes: list[str]) -> lsp.DiagnosticSeverity: 177 | # TODO: All reported issues from linter are treated as warning. 178 | # change it as appropriate for your linter. 179 | return lsp.DiagnosticSeverity.Warning 180 | 181 | 182 | # ********************************************************** 183 | # Linting features end here 184 | # ********************************************************** 185 | 186 | # TODO: If your tool is a formatter then update this section. 187 | # Delete "Formatting features" section if your tool is NOT a 188 | # formatter. 189 | # ********************************************************** 190 | # Formatting features start here 191 | # ********************************************************** 192 | # Sample implementations: 193 | # Black: https://github.com/microsoft/vscode-black-formatter/blob/main/bundled/tool 194 | 195 | 196 | @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_FORMATTING) 197 | def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | None: 198 | """LSP handler for textDocument/formatting request.""" 199 | # If your tool is a formatter you can use this handler to provide 200 | # formatting support on save. You have to return an array of lsp.TextEdit 201 | # objects, to provide your formatted results. 202 | 203 | document = LSP_SERVER.workspace.get_document(params.text_document.uri) 204 | edits = _formatting_helper(document) 205 | if edits: 206 | return edits 207 | 208 | # NOTE: If you provide [] array, VS Code will clear the file of all contents. 209 | # To indicate no changes to file return None. 210 | return None 211 | 212 | 213 | def _formatting_helper(document: workspace.Document) -> list[lsp.TextEdit] | None: 214 | # TODO: For formatting on save support the formatter you use must support 215 | # formatting via stdin. 216 | # Read, and update_run_tool_on_document and _run_tool functions as needed 217 | # for your formatter. 218 | result = _run_tool_on_document(document, use_stdin=True) 219 | if result.stdout: 220 | new_source = _match_line_endings(document, result.stdout) 221 | return [ 222 | lsp.TextEdit( 223 | range=lsp.Range( 224 | start=lsp.Position(line=0, character=0), 225 | end=lsp.Position(line=len(document.lines), character=0), 226 | ), 227 | new_text=new_source, 228 | ) 229 | ] 230 | return None 231 | 232 | 233 | def _get_line_endings(lines: list[str]) -> str: 234 | """Returns line endings used in the text.""" 235 | try: 236 | if lines[0][-2:] == "\r\n": 237 | return "\r\n" 238 | return "\n" 239 | except Exception: # pylint: disable=broad-except 240 | return None 241 | 242 | 243 | def _match_line_endings(document: workspace.Document, text: str) -> str: 244 | """Ensures that the edited text line endings matches the document line endings.""" 245 | expected = _get_line_endings(document.source.splitlines(keepends=True)) 246 | actual = _get_line_endings(text.splitlines(keepends=True)) 247 | if actual == expected or actual is None or expected is None: 248 | return text 249 | return text.replace(actual, expected) 250 | 251 | 252 | # ********************************************************** 253 | # Formatting features ends here 254 | # ********************************************************** 255 | 256 | 257 | # ********************************************************** 258 | # Required Language Server Initialization and Exit handlers. 259 | # ********************************************************** 260 | @LSP_SERVER.feature(lsp.INITIALIZE) 261 | def initialize(params: lsp.InitializeParams) -> None: 262 | """LSP handler for initialize request.""" 263 | log_to_output(f"CWD Server: {os.getcwd()}") 264 | 265 | paths = "\r\n ".join(sys.path) 266 | log_to_output(f"sys.path used to run Server:\r\n {paths}") 267 | 268 | GLOBAL_SETTINGS.update(**params.initialization_options.get("globalSettings", {})) 269 | 270 | settings = params.initialization_options["settings"] 271 | _update_workspace_settings(settings) 272 | log_to_output( 273 | f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n" 274 | ) 275 | log_to_output( 276 | f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n" 277 | ) 278 | 279 | 280 | @LSP_SERVER.feature(lsp.EXIT) 281 | def on_exit(_params: Optional[Any] = None) -> None: 282 | """Handle clean up on exit.""" 283 | jsonrpc.shutdown_json_rpc() 284 | 285 | 286 | @LSP_SERVER.feature(lsp.SHUTDOWN) 287 | def on_shutdown(_params: Optional[Any] = None) -> None: 288 | """Handle clean up on shutdown.""" 289 | jsonrpc.shutdown_json_rpc() 290 | 291 | 292 | def _get_global_defaults(): 293 | return { 294 | "path": GLOBAL_SETTINGS.get("path", []), 295 | "interpreter": GLOBAL_SETTINGS.get("interpreter", [sys.executable]), 296 | "args": GLOBAL_SETTINGS.get("args", []), 297 | "importStrategy": GLOBAL_SETTINGS.get("importStrategy", "useBundled"), 298 | "showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"), 299 | } 300 | 301 | 302 | def _update_workspace_settings(settings): 303 | if not settings: 304 | key = os.getcwd() 305 | WORKSPACE_SETTINGS[key] = { 306 | "cwd": key, 307 | "workspaceFS": key, 308 | "workspace": uris.from_fs_path(key), 309 | **_get_global_defaults(), 310 | } 311 | return 312 | 313 | for setting in settings: 314 | key = uris.to_fs_path(setting["workspace"]) 315 | WORKSPACE_SETTINGS[key] = { 316 | "cwd": key, 317 | **setting, 318 | "workspaceFS": key, 319 | } 320 | 321 | 322 | def _get_settings_by_path(file_path: pathlib.Path): 323 | workspaces = {s["workspaceFS"] for s in WORKSPACE_SETTINGS.values()} 324 | 325 | while file_path != file_path.parent: 326 | str_file_path = str(file_path) 327 | if str_file_path in workspaces: 328 | return WORKSPACE_SETTINGS[str_file_path] 329 | file_path = file_path.parent 330 | 331 | setting_values = list(WORKSPACE_SETTINGS.values()) 332 | return setting_values[0] 333 | 334 | 335 | def _get_document_key(document: workspace.Document): 336 | if WORKSPACE_SETTINGS: 337 | document_workspace = pathlib.Path(document.path) 338 | workspaces = {s["workspaceFS"] for s in WORKSPACE_SETTINGS.values()} 339 | 340 | # Find workspace settings for the given file. 341 | while document_workspace != document_workspace.parent: 342 | if str(document_workspace) in workspaces: 343 | return str(document_workspace) 344 | document_workspace = document_workspace.parent 345 | 346 | return None 347 | 348 | 349 | def _get_settings_by_document(document: workspace.Document | None): 350 | if document is None or document.path is None: 351 | return list(WORKSPACE_SETTINGS.values())[0] 352 | 353 | key = _get_document_key(document) 354 | if key is None: 355 | # This is either a non-workspace file or there is no workspace. 356 | key = os.fspath(pathlib.Path(document.path).parent) 357 | return { 358 | "cwd": key, 359 | "workspaceFS": key, 360 | "workspace": uris.from_fs_path(key), 361 | **_get_global_defaults(), 362 | } 363 | 364 | return WORKSPACE_SETTINGS[str(key)] 365 | 366 | 367 | # ***************************************************** 368 | # Internal execution APIs. 369 | # ***************************************************** 370 | def _run_tool_on_document( 371 | document: workspace.Document, 372 | use_stdin: bool = False, 373 | extra_args: Optional[Sequence[str]] = None, 374 | ) -> utils.RunResult | None: 375 | """Runs tool on the given document. 376 | 377 | if use_stdin is true then contents of the document is passed to the 378 | tool via stdin. 379 | """ 380 | if extra_args is None: 381 | extra_args = [] 382 | if str(document.uri).startswith("vscode-notebook-cell"): 383 | # TODO: Decide on if you want to skip notebook cells. 384 | # Skip notebook cells 385 | return None 386 | 387 | if utils.is_stdlib_file(document.path): 388 | # TODO: Decide on if you want to skip standard library files. 389 | # Skip standard library python files. 390 | return None 391 | 392 | # deep copy here to prevent accidentally updating global settings. 393 | settings = copy.deepcopy(_get_settings_by_document(document)) 394 | 395 | code_workspace = settings["workspaceFS"] 396 | cwd = settings["cwd"] 397 | 398 | use_path = False 399 | use_rpc = False 400 | if settings["path"]: 401 | # 'path' setting takes priority over everything. 402 | use_path = True 403 | argv = settings["path"] 404 | elif settings["interpreter"] and not utils.is_current_interpreter( 405 | settings["interpreter"][0] 406 | ): 407 | # If there is a different interpreter set use JSON-RPC to the subprocess 408 | # running under that interpreter. 409 | argv = [TOOL_MODULE] 410 | use_rpc = True 411 | else: 412 | # if the interpreter is same as the interpreter running this 413 | # process then run as module. 414 | argv = [TOOL_MODULE] 415 | 416 | argv += TOOL_ARGS + settings["args"] + extra_args 417 | 418 | if use_stdin: 419 | # TODO: update these to pass the appropriate arguments to provide document contents 420 | # to tool via stdin. 421 | # For example, for pylint args for stdin looks like this: 422 | # pylint --from-stdin 423 | # Here `--from-stdin` path is used by pylint to make decisions on the file contents 424 | # that are being processed. Like, applying exclusion rules. 425 | # It should look like this when you pass it: 426 | # argv += ["--from-stdin", document.path] 427 | # Read up on how your tool handles contents via stdin. If stdin is not supported use 428 | # set use_stdin to False, or provide path, what ever is appropriate for your tool. 429 | argv += [] 430 | else: 431 | argv += [document.path] 432 | 433 | if use_path: 434 | # This mode is used when running executables. 435 | log_to_output(" ".join(argv)) 436 | log_to_output(f"CWD Server: {cwd}") 437 | result = utils.run_path( 438 | argv=argv, 439 | use_stdin=use_stdin, 440 | cwd=cwd, 441 | source=document.source.replace("\r\n", "\n"), 442 | ) 443 | if result.stderr: 444 | log_to_output(result.stderr) 445 | elif use_rpc: 446 | # This mode is used if the interpreter running this server is different from 447 | # the interpreter used for running this server. 448 | log_to_output(" ".join(settings["interpreter"] + ["-m"] + argv)) 449 | log_to_output(f"CWD Linter: {cwd}") 450 | 451 | result = jsonrpc.run_over_json_rpc( 452 | workspace=code_workspace, 453 | interpreter=settings["interpreter"], 454 | module=TOOL_MODULE, 455 | argv=argv, 456 | use_stdin=use_stdin, 457 | cwd=cwd, 458 | source=document.source, 459 | ) 460 | if result.exception: 461 | log_error(result.exception) 462 | result = utils.RunResult(result.stdout, result.stderr) 463 | elif result.stderr: 464 | log_to_output(result.stderr) 465 | else: 466 | # In this mode the tool is run as a module in the same process as the language server. 467 | log_to_output(" ".join([sys.executable, "-m"] + argv)) 468 | log_to_output(f"CWD Linter: {cwd}") 469 | # This is needed to preserve sys.path, in cases where the tool modifies 470 | # sys.path and that might not work for this scenario next time around. 471 | with utils.substitute_attr(sys, "path", sys.path[:]): 472 | try: 473 | # TODO: `utils.run_module` is equivalent to running `python -m `. 474 | # If your tool supports a programmatic API then replace the function below 475 | # with code for your tool. You can also use `utils.run_api` helper, which 476 | # handles changing working directories, managing io streams, etc. 477 | # Also update `_run_tool` function and `utils.run_module` in `lsp_runner.py`. 478 | result = utils.run_module( 479 | module=TOOL_MODULE, 480 | argv=argv, 481 | use_stdin=use_stdin, 482 | cwd=cwd, 483 | source=document.source, 484 | ) 485 | except Exception: 486 | log_error(traceback.format_exc(chain=True)) 487 | raise 488 | if result.stderr: 489 | log_to_output(result.stderr) 490 | 491 | log_to_output(f"{document.uri} :\r\n{result.stdout}") 492 | return result 493 | 494 | 495 | def _run_tool(extra_args: Sequence[str]) -> utils.RunResult: 496 | """Runs tool.""" 497 | # deep copy here to prevent accidentally updating global settings. 498 | settings = copy.deepcopy(_get_settings_by_document(None)) 499 | 500 | code_workspace = settings["workspaceFS"] 501 | cwd = settings["workspaceFS"] 502 | 503 | use_path = False 504 | use_rpc = False 505 | if len(settings["path"]) > 0: 506 | # 'path' setting takes priority over everything. 507 | use_path = True 508 | argv = settings["path"] 509 | elif len(settings["interpreter"]) > 0 and not utils.is_current_interpreter( 510 | settings["interpreter"][0] 511 | ): 512 | # If there is a different interpreter set use JSON-RPC to the subprocess 513 | # running under that interpreter. 514 | argv = [TOOL_MODULE] 515 | use_rpc = True 516 | else: 517 | # if the interpreter is same as the interpreter running this 518 | # process then run as module. 519 | argv = [TOOL_MODULE] 520 | 521 | argv += extra_args 522 | 523 | if use_path: 524 | # This mode is used when running executables. 525 | log_to_output(" ".join(argv)) 526 | log_to_output(f"CWD Server: {cwd}") 527 | result = utils.run_path(argv=argv, use_stdin=True, cwd=cwd) 528 | if result.stderr: 529 | log_to_output(result.stderr) 530 | elif use_rpc: 531 | # This mode is used if the interpreter running this server is different from 532 | # the interpreter used for running this server. 533 | log_to_output(" ".join(settings["interpreter"] + ["-m"] + argv)) 534 | log_to_output(f"CWD Linter: {cwd}") 535 | result = jsonrpc.run_over_json_rpc( 536 | workspace=code_workspace, 537 | interpreter=settings["interpreter"], 538 | module=TOOL_MODULE, 539 | argv=argv, 540 | use_stdin=True, 541 | cwd=cwd, 542 | ) 543 | if result.exception: 544 | log_error(result.exception) 545 | result = utils.RunResult(result.stdout, result.stderr) 546 | elif result.stderr: 547 | log_to_output(result.stderr) 548 | else: 549 | # In this mode the tool is run as a module in the same process as the language server. 550 | log_to_output(" ".join([sys.executable, "-m"] + argv)) 551 | log_to_output(f"CWD Linter: {cwd}") 552 | # This is needed to preserve sys.path, in cases where the tool modifies 553 | # sys.path and that might not work for this scenario next time around. 554 | with utils.substitute_attr(sys, "path", sys.path[:]): 555 | try: 556 | # TODO: `utils.run_module` is equivalent to running `python -m `. 557 | # If your tool supports a programmatic API then replace the function below 558 | # with code for your tool. You can also use `utils.run_api` helper, which 559 | # handles changing working directories, managing io streams, etc. 560 | # Also update `_run_tool_on_document` function and `utils.run_module` in `lsp_runner.py`. 561 | result = utils.run_module( 562 | module=TOOL_MODULE, argv=argv, use_stdin=True, cwd=cwd 563 | ) 564 | except Exception: 565 | log_error(traceback.format_exc(chain=True)) 566 | raise 567 | if result.stderr: 568 | log_to_output(result.stderr) 569 | 570 | log_to_output(f"\r\n{result.stdout}\r\n") 571 | return result 572 | 573 | 574 | # ***************************************************** 575 | # Logging and notification. 576 | # ***************************************************** 577 | def log_to_output( 578 | message: str, msg_type: lsp.MessageType = lsp.MessageType.Log 579 | ) -> None: 580 | LSP_SERVER.show_message_log(message, msg_type) 581 | 582 | 583 | def log_error(message: str) -> None: 584 | LSP_SERVER.show_message_log(message, lsp.MessageType.Error) 585 | if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onError", "onWarning", "always"]: 586 | LSP_SERVER.show_message(message, lsp.MessageType.Error) 587 | 588 | 589 | def log_warning(message: str) -> None: 590 | LSP_SERVER.show_message_log(message, lsp.MessageType.Warning) 591 | if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onWarning", "always"]: 592 | LSP_SERVER.show_message(message, lsp.MessageType.Warning) 593 | 594 | 595 | def log_always(message: str) -> None: 596 | LSP_SERVER.show_message_log(message, lsp.MessageType.Info) 597 | if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["always"]: 598 | LSP_SERVER.show_message(message, lsp.MessageType.Info) 599 | 600 | 601 | # ***************************************************** 602 | # Start the server. 603 | # ***************************************************** 604 | if __name__ == "__main__": 605 | LSP_SERVER.start_io() 606 | -------------------------------------------------------------------------------- /bundled/tool/lsp_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """Utility functions and classes for use with running tools over LSP.""" 4 | from __future__ import annotations 5 | 6 | import contextlib 7 | import io 8 | import os 9 | import os.path 10 | import runpy 11 | import site 12 | import subprocess 13 | import sys 14 | import threading 15 | from typing import Any, Callable, List, Sequence, Tuple, Union 16 | 17 | # Save the working directory used when loading this module 18 | SERVER_CWD = os.getcwd() 19 | CWD_LOCK = threading.Lock() 20 | 21 | 22 | def as_list(content: Union[Any, List[Any], Tuple[Any]]) -> Union[List[Any], Tuple[Any]]: 23 | """Ensures we always get a list""" 24 | if isinstance(content, (list, tuple)): 25 | return content 26 | return [content] 27 | 28 | 29 | # pylint: disable-next=consider-using-generator 30 | _site_paths = tuple( 31 | [ 32 | os.path.normcase(os.path.normpath(p)) 33 | for p in (as_list(site.getsitepackages()) + as_list(site.getusersitepackages())) 34 | ] 35 | ) 36 | 37 | 38 | def is_same_path(file_path1, file_path2) -> bool: 39 | """Returns true if two paths are the same.""" 40 | return os.path.normcase(os.path.normpath(file_path1)) == os.path.normcase( 41 | os.path.normpath(file_path2) 42 | ) 43 | 44 | 45 | def is_current_interpreter(executable) -> bool: 46 | """Returns true if the executable path is same as the current interpreter.""" 47 | return is_same_path(executable, sys.executable) 48 | 49 | 50 | def is_stdlib_file(file_path) -> bool: 51 | """Return True if the file belongs to standard library.""" 52 | return os.path.normcase(os.path.normpath(file_path)).startswith(_site_paths) 53 | 54 | 55 | # pylint: disable-next=too-few-public-methods 56 | class RunResult: 57 | """Object to hold result from running tool.""" 58 | 59 | def __init__(self, stdout: str, stderr: str): 60 | self.stdout: str = stdout 61 | self.stderr: str = stderr 62 | 63 | 64 | class CustomIO(io.TextIOWrapper): 65 | """Custom stream object to replace stdio.""" 66 | 67 | name = None 68 | 69 | def __init__(self, name, encoding="utf-8", newline=None): 70 | self._buffer = io.BytesIO() 71 | self._buffer.name = name 72 | super().__init__(self._buffer, encoding=encoding, newline=newline) 73 | 74 | def close(self): 75 | """Provide this close method which is used by some tools.""" 76 | # This is intentionally empty. 77 | 78 | def get_value(self) -> str: 79 | """Returns value from the buffer as string.""" 80 | self.seek(0) 81 | return self.read() 82 | 83 | 84 | @contextlib.contextmanager 85 | def substitute_attr(obj: Any, attribute: str, new_value: Any): 86 | """Manage object attributes context when using runpy.run_module().""" 87 | old_value = getattr(obj, attribute) 88 | setattr(obj, attribute, new_value) 89 | yield 90 | setattr(obj, attribute, old_value) 91 | 92 | 93 | @contextlib.contextmanager 94 | def redirect_io(stream: str, new_stream): 95 | """Redirect stdio streams to a custom stream.""" 96 | old_stream = getattr(sys, stream) 97 | setattr(sys, stream, new_stream) 98 | yield 99 | setattr(sys, stream, old_stream) 100 | 101 | 102 | @contextlib.contextmanager 103 | def change_cwd(new_cwd): 104 | """Change working directory before running code.""" 105 | os.chdir(new_cwd) 106 | yield 107 | os.chdir(SERVER_CWD) 108 | 109 | 110 | def _run_module( 111 | module: str, argv: Sequence[str], use_stdin: bool, source: str = None 112 | ) -> RunResult: 113 | """Runs as a module.""" 114 | str_output = CustomIO("", encoding="utf-8") 115 | str_error = CustomIO("", encoding="utf-8") 116 | 117 | with contextlib.suppress(SystemExit): 118 | with substitute_attr(sys, "argv", argv): 119 | with redirect_io("stdout", str_output): 120 | with redirect_io("stderr", str_error): 121 | if use_stdin and source is not None: 122 | str_input = CustomIO("", encoding="utf-8", newline="\n") 123 | with redirect_io("stdin", str_input): 124 | str_input.write(source) 125 | str_input.seek(0) 126 | runpy.run_module(module, run_name="__main__") 127 | else: 128 | runpy.run_module(module, run_name="__main__") 129 | 130 | return RunResult(str_output.get_value(), str_error.get_value()) 131 | 132 | 133 | def run_module( 134 | module: str, argv: Sequence[str], use_stdin: bool, cwd: str, source: str = None 135 | ) -> RunResult: 136 | """Runs as a module.""" 137 | with CWD_LOCK: 138 | if is_same_path(os.getcwd(), cwd): 139 | return _run_module(module, argv, use_stdin, source) 140 | with change_cwd(cwd): 141 | return _run_module(module, argv, use_stdin, source) 142 | 143 | 144 | def run_path( 145 | argv: Sequence[str], use_stdin: bool, cwd: str, source: str = None 146 | ) -> RunResult: 147 | """Runs as an executable.""" 148 | if use_stdin: 149 | with subprocess.Popen( 150 | argv, 151 | encoding="utf-8", 152 | stdout=subprocess.PIPE, 153 | stderr=subprocess.PIPE, 154 | stdin=subprocess.PIPE, 155 | cwd=cwd, 156 | ) as process: 157 | return RunResult(*process.communicate(input=source)) 158 | else: 159 | result = subprocess.run( 160 | argv, 161 | encoding="utf-8", 162 | stdout=subprocess.PIPE, 163 | stderr=subprocess.PIPE, 164 | check=False, 165 | cwd=cwd, 166 | ) 167 | return RunResult(result.stdout, result.stderr) 168 | 169 | 170 | def run_api( 171 | callback: Callable[[Sequence[str], CustomIO, CustomIO, CustomIO | None], None], 172 | argv: Sequence[str], 173 | use_stdin: bool, 174 | cwd: str, 175 | source: str = None, 176 | ) -> RunResult: 177 | """Run a API.""" 178 | with CWD_LOCK: 179 | if is_same_path(os.getcwd(), cwd): 180 | return _run_api(callback, argv, use_stdin, source) 181 | with change_cwd(cwd): 182 | return _run_api(callback, argv, use_stdin, source) 183 | 184 | 185 | def _run_api( 186 | callback: Callable[[Sequence[str], CustomIO, CustomIO, CustomIO | None], None], 187 | argv: Sequence[str], 188 | use_stdin: bool, 189 | source: str = None, 190 | ) -> RunResult: 191 | str_output = CustomIO("", encoding="utf-8") 192 | str_error = CustomIO("", encoding="utf-8") 193 | 194 | with contextlib.suppress(SystemExit): 195 | with substitute_attr(sys, "argv", argv): 196 | with redirect_io("stdout", str_output): 197 | with redirect_io("stderr", str_error): 198 | if use_stdin and source is not None: 199 | str_input = CustomIO("", encoding="utf-8", newline="\n") 200 | with redirect_io("stdin", str_input): 201 | str_input.write(source) 202 | str_input.seek(0) 203 | callback(argv, str_output, str_error, str_input) 204 | else: 205 | callback(argv, str_output, str_error) 206 | 207 | return RunResult(str_output.get_value(), str_error.get_value()) 208 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """All the action we need during build""" 4 | 5 | import json 6 | import os 7 | import pathlib 8 | import urllib.request as url_lib 9 | from typing import List 10 | 11 | import nox # pylint: disable=import-error 12 | 13 | 14 | def _install_bundle(session: nox.Session) -> None: 15 | session.install( 16 | "-t", 17 | "./bundled/libs", 18 | "--no-cache-dir", 19 | "--implementation", 20 | "py", 21 | "--no-deps", 22 | "--upgrade", 23 | "-r", 24 | "./requirements.txt", 25 | ) 26 | 27 | 28 | def _check_files(names: List[str]) -> None: 29 | root_dir = pathlib.Path(__file__).parent 30 | for name in names: 31 | file_path = root_dir / name 32 | lines: List[str] = file_path.read_text().splitlines() 33 | if any(line for line in lines if line.startswith("# TODO:")): 34 | raise Exception(f"Please update {os.fspath(file_path)}.") 35 | 36 | 37 | def _update_pip_packages(session: nox.Session) -> None: 38 | session.run("pip-compile", "--generate-hashes", "--resolver=backtracking", "--upgrade", "./requirements.in") 39 | session.run( 40 | "pip-compile", 41 | "--generate-hashes", 42 | "--resolver=backtracking", 43 | "--upgrade", 44 | "./src/test/python_tests/requirements.in", 45 | ) 46 | 47 | 48 | def _get_package_data(package): 49 | json_uri = f"https://registry.npmjs.org/{package}" 50 | with url_lib.urlopen(json_uri) as response: 51 | return json.loads(response.read()) 52 | 53 | 54 | def _update_npm_packages(session: nox.Session) -> None: 55 | pinned = { 56 | "vscode-languageclient", 57 | "@types/vscode", 58 | "@types/node", 59 | } 60 | package_json_path = pathlib.Path(__file__).parent / "package.json" 61 | package_json = json.loads(package_json_path.read_text(encoding="utf-8")) 62 | 63 | for package in package_json["dependencies"]: 64 | if package not in pinned: 65 | data = _get_package_data(package) 66 | latest = "^" + data["dist-tags"]["latest"] 67 | package_json["dependencies"][package] = latest 68 | 69 | for package in package_json["devDependencies"]: 70 | if package not in pinned: 71 | data = _get_package_data(package) 72 | latest = "^" + data["dist-tags"]["latest"] 73 | package_json["devDependencies"][package] = latest 74 | 75 | # Ensure engine matches the package 76 | if ( 77 | package_json["engines"]["vscode"] 78 | != package_json["devDependencies"]["@types/vscode"] 79 | ): 80 | print( 81 | "Please check VS Code engine version and @types/vscode version in package.json." 82 | ) 83 | 84 | new_package_json = json.dumps(package_json, indent=4) 85 | # JSON dumps uses \n for line ending on all platforms by default 86 | if not new_package_json.endswith("\n"): 87 | new_package_json += "\n" 88 | package_json_path.write_text(new_package_json, encoding="utf-8") 89 | session.run("npm", "install", external=True) 90 | 91 | 92 | def _setup_template_environment(session: nox.Session) -> None: 93 | session.install("wheel", "pip-tools") 94 | _update_pip_packages(session) 95 | _install_bundle(session) 96 | 97 | 98 | @nox.session() 99 | def setup(session: nox.Session) -> None: 100 | """Sets up the template for development.""" 101 | _setup_template_environment(session) 102 | 103 | 104 | @nox.session() 105 | def tests(session: nox.Session) -> None: 106 | """Runs all the tests for the extension.""" 107 | session.install("-r", "src/test/python_tests/requirements.txt") 108 | session.run("pytest", "src/test/python_tests") 109 | 110 | 111 | @nox.session() 112 | def lint(session: nox.Session) -> None: 113 | """Runs linter and formatter checks on python files.""" 114 | session.install("-r", "./requirements.txt") 115 | session.install("-r", "src/test/python_tests/requirements.txt") 116 | 117 | session.install("pylint") 118 | session.run("pylint", "-d", "W0511", "./bundled/tool") 119 | session.run( 120 | "pylint", 121 | "-d", 122 | "W0511", 123 | "--ignore=./src/test/python_tests/test_data", 124 | "./src/test/python_tests", 125 | ) 126 | session.run("pylint", "-d", "W0511", "noxfile.py") 127 | 128 | # check formatting using black 129 | session.install("black") 130 | session.run("black", "--check", "./bundled/tool") 131 | session.run("black", "--check", "./src/test/python_tests") 132 | session.run("black", "--check", "noxfile.py") 133 | 134 | # check import sorting using isort 135 | session.install("isort") 136 | session.run("isort", "--check", "./bundled/tool") 137 | session.run("isort", "--check", "./src/test/python_tests") 138 | session.run("isort", "--check", "noxfile.py") 139 | 140 | # check typescript code 141 | session.run("npm", "run", "lint", external=True) 142 | 143 | 144 | @nox.session() 145 | def build_package(session: nox.Session) -> None: 146 | """Builds VSIX package for publishing.""" 147 | _check_files(["README.md", "LICENSE", "SECURITY.md", "SUPPORT.md"]) 148 | _setup_template_environment(session) 149 | session.run("npm", "install", external=True) 150 | session.run("npm", "run", "vsce-package", external=True) 151 | 152 | 153 | @nox.session() 154 | def update_packages(session: nox.Session) -> None: 155 | """Update pip and npm packages.""" 156 | session.install("wheel", "pip-tools") 157 | _update_pip_packages(session) 158 | _update_npm_packages(session) 159 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "displayName": "", 4 | "description": "Linting support for python files using ``.", 5 | "version": "2022.0.0-dev", 6 | "preview": true, 7 | "serverInfo": { 8 | "name": "", 9 | "module": "" 10 | }, 11 | "publisher": "", 12 | "license": "MIT", 13 | "homepage": "https://github.com//", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com//.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com///issues" 20 | }, 21 | "galleryBanner": { 22 | "color": "#1e415e", 23 | "theme": "dark" 24 | }, 25 | "keywords": [ 26 | "python", 27 | "" 28 | ], 29 | "engines": { 30 | "vscode": "^1.78.0" 31 | }, 32 | "categories": [ 33 | "Programming Languages", 34 | "Linters", 35 | "Formatters" 36 | ], 37 | "extensionDependencies": [ 38 | "ms-python.python" 39 | ], 40 | "capabilities": { 41 | "virtualWorkspaces": { 42 | "supported": false, 43 | "description": "Virtual Workspaces are not supported with ." 44 | } 45 | }, 46 | "activationEvents": [ 47 | "onLanguage:python", 48 | "workspaceContains:*.py" 49 | ], 50 | "main": "./dist/extension.js", 51 | "scripts": { 52 | "vscode:prepublish": "npm run package", 53 | "compile": "webpack", 54 | "watch": "webpack --watch", 55 | "package": "webpack --mode production --devtool source-map --config ./webpack.config.js", 56 | "compile-tests": "tsc -p . --outDir out", 57 | "watch-tests": "tsc -p . -w --outDir out", 58 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 59 | "lint": "eslint src --ext ts", 60 | "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.yml' '.github/**/*.yml'", 61 | "test": "node ./out/test/runTest.js", 62 | "vsce-package": "vsce package -o .vsix" 63 | }, 64 | "contributes": { 65 | "configuration": { 66 | "properties": { 67 | ".args": { 68 | "default": [], 69 | "description": "Arguments passed in. Each argument is a separate item in the array.", 70 | "items": { 71 | "type": "string" 72 | }, 73 | "scope": "resource", 74 | "type": "array" 75 | }, 76 | ".path": { 77 | "default": [], 78 | "description": "When set to a path to binary, extension will use that. NOTE: Using this option may slowdown server response time.", 79 | "scope": "resource", 80 | "items": { 81 | "type": "string" 82 | }, 83 | "type": "array" 84 | }, 85 | ".importStrategy": { 86 | "default": "useBundled", 87 | "description": "Defines where `` is imported from. This setting may be ignored if `.path` is set.", 88 | "enum": [ 89 | "useBundled", 90 | "fromEnvironment" 91 | ], 92 | "enumDescriptions": [ 93 | "Always use the bundled version of ``.", 94 | "Use `` from environment, fallback to bundled version only if `` not available in the environment." 95 | ], 96 | "scope": "window", 97 | "type": "string" 98 | }, 99 | ".interpreter": { 100 | "default": [], 101 | "description": "When set to a path to python executable, extension will use that to launch the server and any subprocess.", 102 | "scope": "resource", 103 | "items": { 104 | "type": "string" 105 | }, 106 | "type": "array" 107 | }, 108 | ".showNotifications": { 109 | "default": "off", 110 | "description": "Controls when notifications are shown by this extension.", 111 | "enum": [ 112 | "off", 113 | "onError", 114 | "onWarning", 115 | "always" 116 | ], 117 | "enumDescriptions": [ 118 | "All notifications are turned off, any errors or warning are still available in the logs.", 119 | "Notifications are shown only in the case of an error.", 120 | "Notifications are shown for errors and warnings.", 121 | "Notifications are show for anything that the server chooses to show." 122 | ], 123 | "scope": "machine", 124 | "type": "string" 125 | } 126 | } 127 | }, 128 | "commands": [ 129 | { 130 | "title": "Restart Server", 131 | "category": "", 132 | "command": ".restart" 133 | } 134 | ] 135 | }, 136 | "dependencies": { 137 | "@vscode/python-extension": "^1.0.5", 138 | "fs-extra": "^11.2.0", 139 | "vscode-languageclient": "^8.1.0" 140 | }, 141 | "devDependencies": { 142 | "@types/fs-extra": "^11.0.4", 143 | "@types/vscode": "1.78.0", 144 | "@types/glob": "^8.1.0", 145 | "@types/node": "20.x", 146 | "@typescript-eslint/eslint-plugin": "^6.17.0", 147 | "@typescript-eslint/parser": "^6.17.0", 148 | "@vscode/test-electron": "^2.3.8", 149 | "@vscode/vsce": "^2.22.0", 150 | "eslint": "^8.56.0", 151 | "glob": "^10.3.10", 152 | "prettier": "^3.1.1", 153 | "typescript": "^5.3.3", 154 | "ts-loader": "^9.5.1", 155 | "webpack": "^5.89.0", 156 | "webpack-cli": "^5.1.4" 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | # This file is used to generate requirements.txt. 2 | # NOTE: 3 | # Use Python 3.9 or greater which ever is the minimum version of the python 4 | # you plan on supporting when creating the environment or using pip-tools. 5 | # Only run the commands below to manully upgrade packages in requirements.txt: 6 | # 1) python -m pip install pip-tools 7 | # 2) pip-compile --generate-hashes --resolver=backtracking --upgrade ./requirements.in 8 | # If you are using nox commands to setup or build package you don't need to 9 | # run the above commands manually. 10 | 11 | # Required packages 12 | pygls 13 | packaging 14 | 15 | # TODO: Add your tool here 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.9 3 | # by the following command: 4 | # 5 | # pip-compile --generate-hashes --output-file=requirements.txt requirements.in 6 | # 7 | attrs==25.3.0 \ 8 | --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \ 9 | --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b 10 | # via 11 | # cattrs 12 | # lsprotocol 13 | cattrs==24.1.3 \ 14 | --hash=sha256:981a6ef05875b5bb0c7fb68885546186d306f10f0f6718fe9b96c226e68821ff \ 15 | --hash=sha256:adf957dddd26840f27ffbd060a6c4dd3b2192c5b7c2c0525ef1bd8131d8a83f5 16 | # via 17 | # lsprotocol 18 | # pygls 19 | exceptiongroup==1.2.2 \ 20 | --hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \ 21 | --hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc 22 | # via cattrs 23 | lsprotocol==2023.0.1 \ 24 | --hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \ 25 | --hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d 26 | # via pygls 27 | packaging==24.2 \ 28 | --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ 29 | --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f 30 | # via -r requirements.in 31 | pygls==1.3.1 \ 32 | --hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \ 33 | --hash=sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e 34 | # via -r requirements.in 35 | typing-extensions==4.13.1 \ 36 | --hash=sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69 \ 37 | --hash=sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff 38 | # via cattrs 39 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.21 -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from 'path'; 5 | 6 | const folderName = path.basename(__dirname); 7 | export const EXTENSION_ROOT_DIR = 8 | folderName === 'common' ? path.dirname(path.dirname(__dirname)) : path.dirname(__dirname); 9 | export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'bundled'); 10 | export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `lsp_server.py`); 11 | export const DEBUG_SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `_debug_server.py`); 12 | -------------------------------------------------------------------------------- /src/common/log/logging.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as util from 'util'; 5 | import { Disposable, LogOutputChannel } from 'vscode'; 6 | 7 | type Arguments = unknown[]; 8 | class OutputChannelLogger { 9 | constructor(private readonly channel: LogOutputChannel) {} 10 | 11 | public traceLog(...data: Arguments): void { 12 | this.channel.appendLine(util.format(...data)); 13 | } 14 | 15 | public traceError(...data: Arguments): void { 16 | this.channel.error(util.format(...data)); 17 | } 18 | 19 | public traceWarn(...data: Arguments): void { 20 | this.channel.warn(util.format(...data)); 21 | } 22 | 23 | public traceInfo(...data: Arguments): void { 24 | this.channel.info(util.format(...data)); 25 | } 26 | 27 | public traceVerbose(...data: Arguments): void { 28 | this.channel.debug(util.format(...data)); 29 | } 30 | } 31 | 32 | let channel: OutputChannelLogger | undefined; 33 | export function registerLogger(logChannel: LogOutputChannel): Disposable { 34 | channel = new OutputChannelLogger(logChannel); 35 | return { 36 | dispose: () => { 37 | channel = undefined; 38 | }, 39 | }; 40 | } 41 | 42 | export function traceLog(...args: Arguments): void { 43 | channel?.traceLog(...args); 44 | } 45 | 46 | export function traceError(...args: Arguments): void { 47 | channel?.traceError(...args); 48 | } 49 | 50 | export function traceWarn(...args: Arguments): void { 51 | channel?.traceWarn(...args); 52 | } 53 | 54 | export function traceInfo(...args: Arguments): void { 55 | channel?.traceInfo(...args); 56 | } 57 | 58 | export function traceVerbose(...args: Arguments): void { 59 | channel?.traceVerbose(...args); 60 | } 61 | -------------------------------------------------------------------------------- /src/common/python.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | /* eslint-disable @typescript-eslint/naming-convention */ 5 | import { commands, Disposable, Event, EventEmitter, Uri } from 'vscode'; 6 | import { traceError, traceLog } from './log/logging'; 7 | import { PythonExtension, ResolvedEnvironment } from '@vscode/python-extension'; 8 | 9 | export interface IInterpreterDetails { 10 | path?: string[]; 11 | resource?: Uri; 12 | } 13 | 14 | const onDidChangePythonInterpreterEvent = new EventEmitter(); 15 | export const onDidChangePythonInterpreter: Event = onDidChangePythonInterpreterEvent.event; 16 | 17 | let _api: PythonExtension | undefined; 18 | async function getPythonExtensionAPI(): Promise { 19 | if (_api) { 20 | return _api; 21 | } 22 | _api = await PythonExtension.api(); 23 | return _api; 24 | } 25 | 26 | export async function initializePython(disposables: Disposable[]): Promise { 27 | try { 28 | const api = await getPythonExtensionAPI(); 29 | 30 | if (api) { 31 | disposables.push( 32 | api.environments.onDidChangeActiveEnvironmentPath((e) => { 33 | onDidChangePythonInterpreterEvent.fire({ path: [e.path], resource: e.resource?.uri }); 34 | }), 35 | ); 36 | 37 | traceLog('Waiting for interpreter from python extension.'); 38 | onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()); 39 | } 40 | } catch (error) { 41 | traceError('Error initializing python: ', error); 42 | } 43 | } 44 | 45 | export async function resolveInterpreter(interpreter: string[]): Promise { 46 | const api = await getPythonExtensionAPI(); 47 | return api?.environments.resolveEnvironment(interpreter[0]); 48 | } 49 | 50 | export async function getInterpreterDetails(resource?: Uri): Promise { 51 | const api = await getPythonExtensionAPI(); 52 | const environment = await api?.environments.resolveEnvironment( 53 | api?.environments.getActiveEnvironmentPath(resource), 54 | ); 55 | if (environment?.executable.uri && checkVersion(environment)) { 56 | return { path: [environment?.executable.uri.fsPath], resource }; 57 | } 58 | return { path: undefined, resource }; 59 | } 60 | 61 | export async function getDebuggerPath(): Promise { 62 | const api = await getPythonExtensionAPI(); 63 | return api?.debug.getDebuggerPackagePath(); 64 | } 65 | 66 | export async function runPythonExtensionCommand(command: string, ...rest: any[]) { 67 | await getPythonExtensionAPI(); 68 | return await commands.executeCommand(command, ...rest); 69 | } 70 | 71 | export function checkVersion(resolved: ResolvedEnvironment | undefined): boolean { 72 | const version = resolved?.version; 73 | if (version?.major === 3 && version?.minor >= 8) { 74 | return true; 75 | } 76 | traceError(`Python version ${version?.major}.${version?.minor} is not supported.`); 77 | traceError(`Selected python path: ${resolved?.executable.uri?.fsPath}`); 78 | traceError('Supported versions are 3.8 and above.'); 79 | return false; 80 | } 81 | -------------------------------------------------------------------------------- /src/common/server.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as fsapi from 'fs-extra'; 5 | import { Disposable, env, LogOutputChannel } from 'vscode'; 6 | import { State } from 'vscode-languageclient'; 7 | import { 8 | LanguageClient, 9 | LanguageClientOptions, 10 | RevealOutputChannelOn, 11 | ServerOptions, 12 | } from 'vscode-languageclient/node'; 13 | import { DEBUG_SERVER_SCRIPT_PATH, SERVER_SCRIPT_PATH } from './constants'; 14 | import { traceError, traceInfo, traceVerbose } from './log/logging'; 15 | import { getDebuggerPath } from './python'; 16 | import { getExtensionSettings, getGlobalSettings, getWorkspaceSettings, ISettings } from './settings'; 17 | import { getLSClientTraceLevel, getProjectRoot } from './utilities'; 18 | import { isVirtualWorkspace } from './vscodeapi'; 19 | 20 | export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }; 21 | 22 | async function createServer( 23 | settings: ISettings, 24 | serverId: string, 25 | serverName: string, 26 | outputChannel: LogOutputChannel, 27 | initializationOptions: IInitOptions, 28 | ): Promise { 29 | const command = settings.interpreter[0]; 30 | const cwd = settings.cwd; 31 | 32 | // Set debugger path needed for debugging python code. 33 | const newEnv = { ...process.env }; 34 | const debuggerPath = await getDebuggerPath(); 35 | const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH); 36 | if (newEnv.USE_DEBUGPY && debuggerPath) { 37 | newEnv.DEBUGPY_PATH = debuggerPath; 38 | } else { 39 | newEnv.USE_DEBUGPY = 'False'; 40 | } 41 | 42 | // Set import strategy 43 | newEnv.LS_IMPORT_STRATEGY = settings.importStrategy; 44 | 45 | // Set notification type 46 | newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications; 47 | 48 | const args = 49 | newEnv.USE_DEBUGPY === 'False' || !isDebugScript 50 | ? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH]) 51 | : settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH]); 52 | traceInfo(`Server run command: ${[command, ...args].join(' ')}`); 53 | 54 | const serverOptions: ServerOptions = { 55 | command, 56 | args, 57 | options: { cwd, env: newEnv }, 58 | }; 59 | 60 | // Options to control the language client 61 | const clientOptions: LanguageClientOptions = { 62 | // Register the server for python documents 63 | documentSelector: isVirtualWorkspace() 64 | ? [{ language: 'python' }] 65 | : [ 66 | { scheme: 'file', language: 'python' }, 67 | { scheme: 'untitled', language: 'python' }, 68 | { scheme: 'vscode-notebook', language: 'python' }, 69 | { scheme: 'vscode-notebook-cell', language: 'python' }, 70 | ], 71 | outputChannel: outputChannel, 72 | traceOutputChannel: outputChannel, 73 | revealOutputChannelOn: RevealOutputChannelOn.Never, 74 | initializationOptions, 75 | }; 76 | 77 | return new LanguageClient(serverId, serverName, serverOptions, clientOptions); 78 | } 79 | 80 | let _disposables: Disposable[] = []; 81 | export async function restartServer( 82 | serverId: string, 83 | serverName: string, 84 | outputChannel: LogOutputChannel, 85 | lsClient?: LanguageClient, 86 | ): Promise { 87 | if (lsClient) { 88 | traceInfo(`Server: Stop requested`); 89 | await lsClient.stop(); 90 | _disposables.forEach((d) => d.dispose()); 91 | _disposables = []; 92 | } 93 | const projectRoot = await getProjectRoot(); 94 | const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true); 95 | 96 | const newLSClient = await createServer(workspaceSetting, serverId, serverName, outputChannel, { 97 | settings: await getExtensionSettings(serverId, true), 98 | globalSettings: await getGlobalSettings(serverId, false), 99 | }); 100 | traceInfo(`Server: Start requested.`); 101 | _disposables.push( 102 | newLSClient.onDidChangeState((e) => { 103 | switch (e.newState) { 104 | case State.Stopped: 105 | traceVerbose(`Server State: Stopped`); 106 | break; 107 | case State.Starting: 108 | traceVerbose(`Server State: Starting`); 109 | break; 110 | case State.Running: 111 | traceVerbose(`Server State: Running`); 112 | break; 113 | } 114 | }), 115 | ); 116 | try { 117 | await newLSClient.start(); 118 | } catch (ex) { 119 | traceError(`Server: Start failed: ${ex}`); 120 | return undefined; 121 | } 122 | 123 | const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel); 124 | await newLSClient.setTrace(level); 125 | return newLSClient; 126 | } 127 | -------------------------------------------------------------------------------- /src/common/settings.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { ConfigurationChangeEvent, ConfigurationScope, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; 5 | import { getInterpreterDetails } from './python'; 6 | import { getConfiguration, getWorkspaceFolders } from './vscodeapi'; 7 | 8 | export interface ISettings { 9 | cwd: string; 10 | workspace: string; 11 | args: string[]; 12 | path: string[]; 13 | interpreter: string[]; 14 | importStrategy: string; 15 | showNotifications: string; 16 | } 17 | 18 | export function getExtensionSettings(namespace: string, includeInterpreter?: boolean): Promise { 19 | return Promise.all(getWorkspaceFolders().map((w) => getWorkspaceSettings(namespace, w, includeInterpreter))); 20 | } 21 | 22 | function resolveVariables(value: string[], workspace?: WorkspaceFolder): string[] { 23 | const substitutions = new Map(); 24 | const home = process.env.HOME || process.env.USERPROFILE; 25 | if (home) { 26 | substitutions.set('${userHome}', home); 27 | } 28 | if (workspace) { 29 | substitutions.set('${workspaceFolder}', workspace.uri.fsPath); 30 | } 31 | substitutions.set('${cwd}', process.cwd()); 32 | getWorkspaceFolders().forEach((w) => { 33 | substitutions.set('${workspaceFolder:' + w.name + '}', w.uri.fsPath); 34 | }); 35 | 36 | return value.map((s) => { 37 | for (const [key, value] of substitutions) { 38 | s = s.replace(key, value); 39 | } 40 | return s; 41 | }); 42 | } 43 | 44 | export function getInterpreterFromSetting(namespace: string, scope?: ConfigurationScope) { 45 | const config = getConfiguration(namespace, scope); 46 | return config.get('interpreter'); 47 | } 48 | 49 | export async function getWorkspaceSettings( 50 | namespace: string, 51 | workspace: WorkspaceFolder, 52 | includeInterpreter?: boolean, 53 | ): Promise { 54 | const config = getConfiguration(namespace, workspace.uri); 55 | 56 | let interpreter: string[] = []; 57 | if (includeInterpreter) { 58 | interpreter = getInterpreterFromSetting(namespace, workspace) ?? []; 59 | if (interpreter.length === 0) { 60 | interpreter = (await getInterpreterDetails(workspace.uri)).path ?? []; 61 | } 62 | } 63 | 64 | const workspaceSetting = { 65 | cwd: workspace.uri.fsPath, 66 | workspace: workspace.uri.toString(), 67 | args: resolveVariables(config.get(`args`) ?? [], workspace), 68 | path: resolveVariables(config.get(`path`) ?? [], workspace), 69 | interpreter: resolveVariables(interpreter, workspace), 70 | importStrategy: config.get(`importStrategy`) ?? 'useBundled', 71 | showNotifications: config.get(`showNotifications`) ?? 'off', 72 | }; 73 | return workspaceSetting; 74 | } 75 | 76 | function getGlobalValue(config: WorkspaceConfiguration, key: string, defaultValue: T): T { 77 | const inspect = config.inspect(key); 78 | return inspect?.globalValue ?? inspect?.defaultValue ?? defaultValue; 79 | } 80 | 81 | export async function getGlobalSettings(namespace: string, includeInterpreter?: boolean): Promise { 82 | const config = getConfiguration(namespace); 83 | 84 | let interpreter: string[] = []; 85 | if (includeInterpreter) { 86 | interpreter = getGlobalValue(config, 'interpreter', []); 87 | if (interpreter === undefined || interpreter.length === 0) { 88 | interpreter = (await getInterpreterDetails()).path ?? []; 89 | } 90 | } 91 | 92 | const setting = { 93 | cwd: process.cwd(), 94 | workspace: process.cwd(), 95 | args: getGlobalValue(config, 'args', []), 96 | path: getGlobalValue(config, 'path', []), 97 | interpreter: interpreter, 98 | importStrategy: getGlobalValue(config, 'importStrategy', 'useBundled'), 99 | showNotifications: getGlobalValue(config, 'showNotifications', 'off'), 100 | }; 101 | return setting; 102 | } 103 | 104 | export function checkIfConfigurationChanged(e: ConfigurationChangeEvent, namespace: string): boolean { 105 | const settings = [ 106 | `${namespace}.args`, 107 | `${namespace}.path`, 108 | `${namespace}.interpreter`, 109 | `${namespace}.importStrategy`, 110 | `${namespace}.showNotifications`, 111 | ]; 112 | const changed = settings.map((s) => e.affectsConfiguration(s)); 113 | return changed.includes(true); 114 | } 115 | -------------------------------------------------------------------------------- /src/common/setup.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from 'path'; 5 | import * as fs from 'fs-extra'; 6 | import { EXTENSION_ROOT_DIR } from './constants'; 7 | 8 | export interface IServerInfo { 9 | name: string; 10 | module: string; 11 | } 12 | 13 | export function loadServerDefaults(): IServerInfo { 14 | const packageJson = path.join(EXTENSION_ROOT_DIR, 'package.json'); 15 | const content = fs.readFileSync(packageJson).toString(); 16 | const config = JSON.parse(content); 17 | return config.serverInfo as IServerInfo; 18 | } 19 | -------------------------------------------------------------------------------- /src/common/utilities.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as fs from 'fs-extra'; 5 | import * as path from 'path'; 6 | import { LogLevel, Uri, WorkspaceFolder } from 'vscode'; 7 | import { Trace } from 'vscode-jsonrpc/node'; 8 | import { getWorkspaceFolders } from './vscodeapi'; 9 | 10 | function logLevelToTrace(logLevel: LogLevel): Trace { 11 | switch (logLevel) { 12 | case LogLevel.Error: 13 | case LogLevel.Warning: 14 | case LogLevel.Info: 15 | return Trace.Messages; 16 | 17 | case LogLevel.Debug: 18 | case LogLevel.Trace: 19 | return Trace.Verbose; 20 | 21 | case LogLevel.Off: 22 | default: 23 | return Trace.Off; 24 | } 25 | } 26 | 27 | export function getLSClientTraceLevel(channelLogLevel: LogLevel, globalLogLevel: LogLevel): Trace { 28 | if (channelLogLevel === LogLevel.Off) { 29 | return logLevelToTrace(globalLogLevel); 30 | } 31 | if (globalLogLevel === LogLevel.Off) { 32 | return logLevelToTrace(channelLogLevel); 33 | } 34 | const level = logLevelToTrace(channelLogLevel <= globalLogLevel ? channelLogLevel : globalLogLevel); 35 | return level; 36 | } 37 | 38 | export async function getProjectRoot(): Promise { 39 | const workspaces: readonly WorkspaceFolder[] = getWorkspaceFolders(); 40 | if (workspaces.length === 0) { 41 | return { 42 | uri: Uri.file(process.cwd()), 43 | name: path.basename(process.cwd()), 44 | index: 0, 45 | }; 46 | } else if (workspaces.length === 1) { 47 | return workspaces[0]; 48 | } else { 49 | let rootWorkspace = workspaces[0]; 50 | let root = undefined; 51 | for (const w of workspaces) { 52 | if (await fs.pathExists(w.uri.fsPath)) { 53 | root = w.uri.fsPath; 54 | rootWorkspace = w; 55 | break; 56 | } 57 | } 58 | 59 | for (const w of workspaces) { 60 | if (root && root.length > w.uri.fsPath.length && (await fs.pathExists(w.uri.fsPath))) { 61 | root = w.uri.fsPath; 62 | rootWorkspace = w; 63 | } 64 | } 65 | return rootWorkspace; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/common/vscodeapi.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 5 | /* eslint-disable @typescript-eslint/no-explicit-any */ 6 | import { 7 | commands, 8 | ConfigurationScope, 9 | Disposable, 10 | LogOutputChannel, 11 | Uri, 12 | window, 13 | workspace, 14 | WorkspaceConfiguration, 15 | WorkspaceFolder, 16 | } from 'vscode'; 17 | 18 | export function createOutputChannel(name: string): LogOutputChannel { 19 | return window.createOutputChannel(name, { log: true }); 20 | } 21 | 22 | export function getConfiguration(config: string, scope?: ConfigurationScope): WorkspaceConfiguration { 23 | return workspace.getConfiguration(config, scope); 24 | } 25 | 26 | export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { 27 | return commands.registerCommand(command, callback, thisArg); 28 | } 29 | 30 | export const { onDidChangeConfiguration } = workspace; 31 | 32 | export function isVirtualWorkspace(): boolean { 33 | const isVirtual = workspace.workspaceFolders && workspace.workspaceFolders.every((f) => f.uri.scheme !== 'file'); 34 | return !!isVirtual; 35 | } 36 | 37 | export function getWorkspaceFolders(): readonly WorkspaceFolder[] { 38 | return workspace.workspaceFolders ?? []; 39 | } 40 | 41 | export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { 42 | return workspace.getWorkspaceFolder(uri); 43 | } 44 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from 'vscode'; 5 | import { LanguageClient } from 'vscode-languageclient/node'; 6 | import { registerLogger, traceError, traceLog, traceVerbose } from './common/log/logging'; 7 | import { 8 | checkVersion, 9 | getInterpreterDetails, 10 | initializePython, 11 | onDidChangePythonInterpreter, 12 | resolveInterpreter, 13 | } from './common/python'; 14 | import { restartServer } from './common/server'; 15 | import { checkIfConfigurationChanged, getInterpreterFromSetting } from './common/settings'; 16 | import { loadServerDefaults } from './common/setup'; 17 | import { getLSClientTraceLevel } from './common/utilities'; 18 | import { createOutputChannel, onDidChangeConfiguration, registerCommand } from './common/vscodeapi'; 19 | 20 | let lsClient: LanguageClient | undefined; 21 | export async function activate(context: vscode.ExtensionContext): Promise { 22 | // This is required to get server name and module. This should be 23 | // the first thing that we do in this extension. 24 | const serverInfo = loadServerDefaults(); 25 | const serverName = serverInfo.name; 26 | const serverId = serverInfo.module; 27 | 28 | // Setup logging 29 | const outputChannel = createOutputChannel(serverName); 30 | context.subscriptions.push(outputChannel, registerLogger(outputChannel)); 31 | 32 | const changeLogLevel = async (c: vscode.LogLevel, g: vscode.LogLevel) => { 33 | const level = getLSClientTraceLevel(c, g); 34 | await lsClient?.setTrace(level); 35 | }; 36 | 37 | context.subscriptions.push( 38 | outputChannel.onDidChangeLogLevel(async (e) => { 39 | await changeLogLevel(e, vscode.env.logLevel); 40 | }), 41 | vscode.env.onDidChangeLogLevel(async (e) => { 42 | await changeLogLevel(outputChannel.logLevel, e); 43 | }), 44 | ); 45 | 46 | // Log Server information 47 | traceLog(`Name: ${serverInfo.name}`); 48 | traceLog(`Module: ${serverInfo.module}`); 49 | traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`); 50 | 51 | const runServer = async () => { 52 | const interpreter = getInterpreterFromSetting(serverId); 53 | if (interpreter && interpreter.length > 0) { 54 | if (checkVersion(await resolveInterpreter(interpreter))) { 55 | traceVerbose(`Using interpreter from ${serverInfo.module}.interpreter: ${interpreter.join(' ')}`); 56 | lsClient = await restartServer(serverId, serverName, outputChannel, lsClient); 57 | } 58 | return; 59 | } 60 | 61 | const interpreterDetails = await getInterpreterDetails(); 62 | if (interpreterDetails.path) { 63 | traceVerbose(`Using interpreter from Python extension: ${interpreterDetails.path.join(' ')}`); 64 | lsClient = await restartServer(serverId, serverName, outputChannel, lsClient); 65 | return; 66 | } 67 | 68 | traceError( 69 | 'Python interpreter missing:\r\n' + 70 | '[Option 1] Select python interpreter using the ms-python.python.\r\n' + 71 | `[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n` + 72 | 'Please use Python 3.8 or greater.', 73 | ); 74 | }; 75 | 76 | context.subscriptions.push( 77 | onDidChangePythonInterpreter(async () => { 78 | await runServer(); 79 | }), 80 | onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { 81 | if (checkIfConfigurationChanged(e, serverId)) { 82 | await runServer(); 83 | } 84 | }), 85 | registerCommand(`${serverId}.restart`, async () => { 86 | await runServer(); 87 | }), 88 | ); 89 | 90 | setImmediate(async () => { 91 | const interpreter = getInterpreterFromSetting(serverId); 92 | if (interpreter === undefined || interpreter.length === 0) { 93 | traceLog(`Python extension loading`); 94 | await initializePython(context.subscriptions); 95 | traceLog(`Python extension loaded`); 96 | } else { 97 | await runServer(); 98 | } 99 | }); 100 | } 101 | 102 | export async function deactivate(): Promise { 103 | if (lsClient) { 104 | await lsClient.stop(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/python_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Constants for use with tests. 5 | """ 6 | import pathlib 7 | 8 | TEST_ROOT = pathlib.Path(__file__).parent.parent 9 | PROJECT_ROOT = TEST_ROOT.parent.parent.parent 10 | TEST_DATA = TEST_ROOT / "test_data" 11 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/defaults.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Default initialize request params. 5 | """ 6 | 7 | import os 8 | 9 | from .constants import PROJECT_ROOT 10 | from .utils import as_uri, get_initialization_options 11 | 12 | VSCODE_DEFAULT_INITIALIZE = { 13 | "processId": os.getpid(), 14 | "clientInfo": {"name": "vscode", "version": "1.45.0"}, 15 | "rootPath": str(PROJECT_ROOT), 16 | "rootUri": as_uri(str(PROJECT_ROOT)), 17 | "capabilities": { 18 | "workspace": { 19 | "applyEdit": True, 20 | "workspaceEdit": { 21 | "documentChanges": True, 22 | "resourceOperations": ["create", "rename", "delete"], 23 | "failureHandling": "textOnlyTransactional", 24 | }, 25 | "didChangeConfiguration": {"dynamicRegistration": True}, 26 | "didChangeWatchedFiles": {"dynamicRegistration": True}, 27 | "symbol": { 28 | "dynamicRegistration": True, 29 | "symbolKind": { 30 | "valueSet": [ 31 | 1, 32 | 2, 33 | 3, 34 | 4, 35 | 5, 36 | 6, 37 | 7, 38 | 8, 39 | 9, 40 | 10, 41 | 11, 42 | 12, 43 | 13, 44 | 14, 45 | 15, 46 | 16, 47 | 17, 48 | 18, 49 | 19, 50 | 20, 51 | 21, 52 | 22, 53 | 23, 54 | 24, 55 | 25, 56 | 26, 57 | ] 58 | }, 59 | "tagSupport": {"valueSet": [1]}, 60 | }, 61 | "executeCommand": {"dynamicRegistration": True}, 62 | "configuration": True, 63 | "workspaceFolders": True, 64 | }, 65 | "textDocument": { 66 | "publishDiagnostics": { 67 | "relatedInformation": True, 68 | "versionSupport": False, 69 | "tagSupport": {"valueSet": [1, 2]}, 70 | "complexDiagnosticCodeSupport": True, 71 | }, 72 | "synchronization": { 73 | "dynamicRegistration": True, 74 | "willSave": True, 75 | "willSaveWaitUntil": True, 76 | "didSave": True, 77 | }, 78 | "completion": { 79 | "dynamicRegistration": True, 80 | "contextSupport": True, 81 | "completionItem": { 82 | "snippetSupport": True, 83 | "commitCharactersSupport": True, 84 | "documentationFormat": ["markdown", "plaintext"], 85 | "deprecatedSupport": True, 86 | "preselectSupport": True, 87 | "tagSupport": {"valueSet": [1]}, 88 | "insertReplaceSupport": True, 89 | }, 90 | "completionItemKind": { 91 | "valueSet": [ 92 | 1, 93 | 2, 94 | 3, 95 | 4, 96 | 5, 97 | 6, 98 | 7, 99 | 8, 100 | 9, 101 | 10, 102 | 11, 103 | 12, 104 | 13, 105 | 14, 106 | 15, 107 | 16, 108 | 17, 109 | 18, 110 | 19, 111 | 20, 112 | 21, 113 | 22, 114 | 23, 115 | 24, 116 | 25, 117 | ] 118 | }, 119 | }, 120 | "hover": { 121 | "dynamicRegistration": True, 122 | "contentFormat": ["markdown", "plaintext"], 123 | }, 124 | "signatureHelp": { 125 | "dynamicRegistration": True, 126 | "signatureInformation": { 127 | "documentationFormat": ["markdown", "plaintext"], 128 | "parameterInformation": {"labelOffsetSupport": True}, 129 | }, 130 | "contextSupport": True, 131 | }, 132 | "definition": {"dynamicRegistration": True, "linkSupport": True}, 133 | "references": {"dynamicRegistration": True}, 134 | "documentHighlight": {"dynamicRegistration": True}, 135 | "documentSymbol": { 136 | "dynamicRegistration": True, 137 | "symbolKind": { 138 | "valueSet": [ 139 | 1, 140 | 2, 141 | 3, 142 | 4, 143 | 5, 144 | 6, 145 | 7, 146 | 8, 147 | 9, 148 | 10, 149 | 11, 150 | 12, 151 | 13, 152 | 14, 153 | 15, 154 | 16, 155 | 17, 156 | 18, 157 | 19, 158 | 20, 159 | 21, 160 | 22, 161 | 23, 162 | 24, 163 | 25, 164 | 26, 165 | ] 166 | }, 167 | "hierarchicalDocumentSymbolSupport": True, 168 | "tagSupport": {"valueSet": [1]}, 169 | }, 170 | "codeAction": { 171 | "dynamicRegistration": True, 172 | "isPreferredSupport": True, 173 | "codeActionLiteralSupport": { 174 | "codeActionKind": { 175 | "valueSet": [ 176 | "", 177 | "quickfix", 178 | "refactor", 179 | "refactor.extract", 180 | "refactor.inline", 181 | "refactor.rewrite", 182 | "source", 183 | "source.organizeImports", 184 | ] 185 | } 186 | }, 187 | }, 188 | "codeLens": {"dynamicRegistration": True}, 189 | "formatting": {"dynamicRegistration": True}, 190 | "rangeFormatting": {"dynamicRegistration": True}, 191 | "onTypeFormatting": {"dynamicRegistration": True}, 192 | "rename": {"dynamicRegistration": True, "prepareSupport": True}, 193 | "documentLink": { 194 | "dynamicRegistration": True, 195 | "tooltipSupport": True, 196 | }, 197 | "typeDefinition": { 198 | "dynamicRegistration": True, 199 | "linkSupport": True, 200 | }, 201 | "implementation": { 202 | "dynamicRegistration": True, 203 | "linkSupport": True, 204 | }, 205 | "colorProvider": {"dynamicRegistration": True}, 206 | "foldingRange": { 207 | "dynamicRegistration": True, 208 | "rangeLimit": 5000, 209 | "lineFoldingOnly": True, 210 | }, 211 | "declaration": {"dynamicRegistration": True, "linkSupport": True}, 212 | "selectionRange": {"dynamicRegistration": True}, 213 | }, 214 | "window": {"workDoneProgress": True}, 215 | }, 216 | "trace": "verbose", 217 | "workspaceFolders": [{"uri": as_uri(str(PROJECT_ROOT)), "name": "my_project"}], 218 | "initializationOptions": get_initialization_options(), 219 | } 220 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/session.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | LSP session client for testing. 5 | """ 6 | 7 | import os 8 | import subprocess 9 | import sys 10 | from concurrent.futures import Future, ThreadPoolExecutor 11 | from threading import Event 12 | 13 | from pyls_jsonrpc.dispatchers import MethodDispatcher 14 | from pyls_jsonrpc.endpoint import Endpoint 15 | from pyls_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter 16 | 17 | from .constants import PROJECT_ROOT 18 | from .defaults import VSCODE_DEFAULT_INITIALIZE 19 | 20 | LSP_EXIT_TIMEOUT = 5000 21 | 22 | 23 | PUBLISH_DIAGNOSTICS = "textDocument/publishDiagnostics" 24 | WINDOW_LOG_MESSAGE = "window/logMessage" 25 | WINDOW_SHOW_MESSAGE = "window/showMessage" 26 | 27 | 28 | # pylint: disable=too-many-instance-attributes 29 | class LspSession(MethodDispatcher): 30 | """Send and Receive messages over LSP as a test LS Client.""" 31 | 32 | def __init__(self, cwd=None, script=None): 33 | self.cwd = cwd if cwd else os.getcwd() 34 | # pylint: disable=consider-using-with 35 | self._thread_pool = ThreadPoolExecutor() 36 | self._sub = None 37 | self._writer = None 38 | self._reader = None 39 | self._endpoint = None 40 | self._notification_callbacks = {} 41 | self.script = ( 42 | script if script else (PROJECT_ROOT / "bundled" / "tool" / "lsp_server.py") 43 | ) 44 | 45 | def __enter__(self): 46 | """Context manager entrypoint. 47 | 48 | shell=True needed for pytest-cov to work in subprocess. 49 | """ 50 | # pylint: disable=consider-using-with 51 | self._sub = subprocess.Popen( 52 | [sys.executable, str(self.script)], 53 | stdout=subprocess.PIPE, 54 | stdin=subprocess.PIPE, 55 | bufsize=0, 56 | cwd=self.cwd, 57 | env=os.environ, 58 | shell="WITH_COVERAGE" in os.environ, 59 | ) 60 | 61 | self._writer = JsonRpcStreamWriter(os.fdopen(self._sub.stdin.fileno(), "wb")) 62 | self._reader = JsonRpcStreamReader(os.fdopen(self._sub.stdout.fileno(), "rb")) 63 | 64 | dispatcher = { 65 | PUBLISH_DIAGNOSTICS: self._publish_diagnostics, 66 | WINDOW_SHOW_MESSAGE: self._window_show_message, 67 | WINDOW_LOG_MESSAGE: self._window_log_message, 68 | } 69 | self._endpoint = Endpoint(dispatcher, self._writer.write) 70 | self._thread_pool.submit(self._reader.listen, self._endpoint.consume) 71 | return self 72 | 73 | def __exit__(self, typ, value, _tb): 74 | self.shutdown(True) 75 | try: 76 | self._sub.terminate() 77 | except Exception: # pylint:disable=broad-except 78 | pass 79 | self._endpoint.shutdown() 80 | self._thread_pool.shutdown() 81 | 82 | def initialize( 83 | self, 84 | initialize_params=None, 85 | process_server_capabilities=None, 86 | ): 87 | """Sends the initialize request to LSP server.""" 88 | if initialize_params is None: 89 | initialize_params = VSCODE_DEFAULT_INITIALIZE 90 | server_initialized = Event() 91 | 92 | def _after_initialize(fut): 93 | if process_server_capabilities: 94 | process_server_capabilities(fut.result()) 95 | self.initialized() 96 | server_initialized.set() 97 | 98 | self._send_request( 99 | "initialize", 100 | params=( 101 | initialize_params 102 | if initialize_params is not None 103 | else VSCODE_DEFAULT_INITIALIZE 104 | ), 105 | handle_response=_after_initialize, 106 | ) 107 | 108 | server_initialized.wait() 109 | 110 | def initialized(self, initialized_params=None): 111 | """Sends the initialized notification to LSP server.""" 112 | self._endpoint.notify("initialized", initialized_params or {}) 113 | 114 | def shutdown(self, should_exit, exit_timeout=LSP_EXIT_TIMEOUT): 115 | """Sends the shutdown request to LSP server.""" 116 | 117 | def _after_shutdown(_): 118 | if should_exit: 119 | self.exit_lsp(exit_timeout) 120 | 121 | self._send_request("shutdown", handle_response=_after_shutdown) 122 | 123 | def exit_lsp(self, exit_timeout=LSP_EXIT_TIMEOUT): 124 | """Handles LSP server process exit.""" 125 | self._endpoint.notify("exit") 126 | assert self._sub.wait(exit_timeout) == 0 127 | 128 | def notify_did_change(self, did_change_params): 129 | """Sends did change notification to LSP Server.""" 130 | self._send_notification("textDocument/didChange", params=did_change_params) 131 | 132 | def notify_did_save(self, did_save_params): 133 | """Sends did save notification to LSP Server.""" 134 | self._send_notification("textDocument/didSave", params=did_save_params) 135 | 136 | def notify_did_open(self, did_open_params): 137 | """Sends did open notification to LSP Server.""" 138 | self._send_notification("textDocument/didOpen", params=did_open_params) 139 | 140 | def notify_did_close(self, did_close_params): 141 | """Sends did close notification to LSP Server.""" 142 | self._send_notification("textDocument/didClose", params=did_close_params) 143 | 144 | def text_document_formatting(self, formatting_params): 145 | """Sends text document references request to LSP server.""" 146 | fut = self._send_request("textDocument/formatting", params=formatting_params) 147 | return fut.result() 148 | 149 | def text_document_code_action(self, code_action_params): 150 | """Sends text document code actions request to LSP server.""" 151 | fut = self._send_request("textDocument/codeAction", params=code_action_params) 152 | return fut.result() 153 | 154 | def code_action_resolve(self, code_action_resolve_params): 155 | """Sends text document code actions resolve request to LSP server.""" 156 | fut = self._send_request( 157 | "codeAction/resolve", params=code_action_resolve_params 158 | ) 159 | return fut.result() 160 | 161 | def set_notification_callback(self, notification_name, callback): 162 | """Set custom LS notification handler.""" 163 | self._notification_callbacks[notification_name] = callback 164 | 165 | def get_notification_callback(self, notification_name): 166 | """Gets callback if set or default callback for a given LS 167 | notification.""" 168 | try: 169 | return self._notification_callbacks[notification_name] 170 | except KeyError: 171 | 172 | def _default_handler(_params): 173 | """Default notification handler.""" 174 | 175 | return _default_handler 176 | 177 | def _publish_diagnostics(self, publish_diagnostics_params): 178 | """Internal handler for text document publish diagnostics.""" 179 | return self._handle_notification( 180 | PUBLISH_DIAGNOSTICS, publish_diagnostics_params 181 | ) 182 | 183 | def _window_log_message(self, window_log_message_params): 184 | """Internal handler for window log message.""" 185 | return self._handle_notification(WINDOW_LOG_MESSAGE, window_log_message_params) 186 | 187 | def _window_show_message(self, window_show_message_params): 188 | """Internal handler for window show message.""" 189 | return self._handle_notification( 190 | WINDOW_SHOW_MESSAGE, window_show_message_params 191 | ) 192 | 193 | def _handle_notification(self, notification_name, params): 194 | """Internal handler for notifications.""" 195 | fut = Future() 196 | 197 | def _handler(): 198 | callback = self.get_notification_callback(notification_name) 199 | callback(params) 200 | fut.set_result(None) 201 | 202 | self._thread_pool.submit(_handler) 203 | return fut 204 | 205 | def _send_request(self, name, params=None, handle_response=lambda f: f.done()): 206 | """Sends {name} request to the LSP server.""" 207 | fut = self._endpoint.request(name, params) 208 | fut.add_done_callback(handle_response) 209 | return fut 210 | 211 | def _send_notification(self, name, params=None): 212 | """Sends {name} notification to the LSP server.""" 213 | self._endpoint.notify(name, params) 214 | -------------------------------------------------------------------------------- /src/test/python_tests/lsp_test_client/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Utility functions for use with tests. 5 | """ 6 | import json 7 | import os 8 | import pathlib 9 | import platform 10 | from random import choice 11 | 12 | from .constants import PROJECT_ROOT 13 | 14 | 15 | def normalizecase(path: str) -> str: 16 | """Fixes 'file' uri or path case for easier testing in windows.""" 17 | if platform.system() == "Windows": 18 | return path.lower() 19 | return path 20 | 21 | 22 | def as_uri(path: str) -> str: 23 | """Return 'file' uri as string.""" 24 | return normalizecase(pathlib.Path(path).as_uri()) 25 | 26 | 27 | class PythonFile: 28 | """Create python file on demand for testing.""" 29 | 30 | def __init__(self, contents, root): 31 | self.contents = contents 32 | self.basename = "".join( 33 | choice("abcdefghijklmnopqrstuvwxyz") if i < 8 else ".py" for i in range(9) 34 | ) 35 | self.fullpath = os.path.join(root, self.basename) 36 | 37 | def __enter__(self): 38 | """Creates a python file for testing.""" 39 | with open(self.fullpath, "w", encoding="utf8") as py_file: 40 | py_file.write(self.contents) 41 | return self 42 | 43 | def __exit__(self, typ, value, _tb): 44 | """Cleans up and deletes the python file.""" 45 | os.unlink(self.fullpath) 46 | 47 | 48 | def get_server_info_defaults(): 49 | """Returns server info from package.json""" 50 | package_json_path = PROJECT_ROOT / "package.json" 51 | package_json = json.loads(package_json_path.read_text()) 52 | return package_json["serverInfo"] 53 | 54 | 55 | def get_initialization_options(): 56 | """Returns initialization options from package.json""" 57 | package_json_path = PROJECT_ROOT / "package.json" 58 | package_json = json.loads(package_json_path.read_text()) 59 | 60 | server_info = package_json["serverInfo"] 61 | server_id = server_info["module"] 62 | 63 | properties = package_json["contributes"]["configuration"]["properties"] 64 | setting = {} 65 | for prop in properties: 66 | name = prop[len(server_id) + 1 :] 67 | value = properties[prop]["default"] 68 | setting[name] = value 69 | 70 | setting["workspace"] = as_uri(str(PROJECT_ROOT)) 71 | setting["interpreter"] = [] 72 | 73 | return {"settings": [setting]} 74 | -------------------------------------------------------------------------------- /src/test/python_tests/requirements.in: -------------------------------------------------------------------------------- 1 | # This file is used to generate ./src/test/python_tests/requirements.txt. 2 | # NOTE: 3 | # Use Python 3.8 or greater which ever is the minimum version of the python 4 | # you plan on supporting when creating the environment or using pip-tools. 5 | # Only run the commands below to manully upgrade packages in requirements.txt: 6 | # 1) python -m pip install pip-tools 7 | # 2) pip-compile --generate-hashes --upgrade ./src/test/python_tests/requirements.in 8 | # If you are using nox commands to setup or build package you don't need to 9 | # run the above commands manually. 10 | 11 | # Packages needed by the testing framework. 12 | pytest 13 | PyHamcrest 14 | python-jsonrpc-server 15 | colorama 16 | -------------------------------------------------------------------------------- /src/test/python_tests/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.9 3 | # by the following command: 4 | # 5 | # pip-compile --generate-hashes ./src/test/python_tests/requirements.in 6 | # 7 | colorama==0.4.6 \ 8 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 9 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 10 | # via -r ./src/test/python_tests/requirements.in 11 | exceptiongroup==1.2.2 \ 12 | --hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \ 13 | --hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc 14 | # via pytest 15 | iniconfig==2.1.0 \ 16 | --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ 17 | --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 18 | # via pytest 19 | packaging==24.2 \ 20 | --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ 21 | --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f 22 | # via pytest 23 | pluggy==1.5.0 \ 24 | --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ 25 | --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 26 | # via pytest 27 | pyhamcrest==2.1.0 \ 28 | --hash=sha256:c6acbec0923d0cb7e72c22af1926f3e7c97b8e8d69fc7498eabacaf7c975bd9c \ 29 | --hash=sha256:f6913d2f392e30e0375b3ecbd7aee79e5d1faa25d345c8f4ff597665dcac2587 30 | # via -r ./src/test/python_tests/requirements.in 31 | pytest==8.3.5 \ 32 | --hash=sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820 \ 33 | --hash=sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845 34 | # via -r ./src/test/python_tests/requirements.in 35 | python-jsonrpc-server==0.4.0 \ 36 | --hash=sha256:62c543e541f101ec5b57dc654efc212d2c2e3ea47ff6f54b2e7dcb36ecf20595 \ 37 | --hash=sha256:e5a908ff182e620aac07db5f57887eeb0afe33993008f57dc1b85b594cea250c 38 | # via -r ./src/test/python_tests/requirements.in 39 | tomli==2.2.1 \ 40 | --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ 41 | --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ 42 | --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \ 43 | --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \ 44 | --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \ 45 | --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \ 46 | --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \ 47 | --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \ 48 | --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \ 49 | --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \ 50 | --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \ 51 | --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \ 52 | --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \ 53 | --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \ 54 | --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \ 55 | --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \ 56 | --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \ 57 | --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \ 58 | --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \ 59 | --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \ 60 | --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \ 61 | --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \ 62 | --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \ 63 | --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \ 64 | --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \ 65 | --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \ 66 | --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \ 67 | --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \ 68 | --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \ 69 | --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ 70 | --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ 71 | --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 72 | # via pytest 73 | ujson==5.10.0 \ 74 | --hash=sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e \ 75 | --hash=sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b \ 76 | --hash=sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6 \ 77 | --hash=sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7 \ 78 | --hash=sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9 \ 79 | --hash=sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd \ 80 | --hash=sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569 \ 81 | --hash=sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f \ 82 | --hash=sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51 \ 83 | --hash=sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20 \ 84 | --hash=sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1 \ 85 | --hash=sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf \ 86 | --hash=sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc \ 87 | --hash=sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e \ 88 | --hash=sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a \ 89 | --hash=sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539 \ 90 | --hash=sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27 \ 91 | --hash=sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165 \ 92 | --hash=sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126 \ 93 | --hash=sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1 \ 94 | --hash=sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816 \ 95 | --hash=sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64 \ 96 | --hash=sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8 \ 97 | --hash=sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e \ 98 | --hash=sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287 \ 99 | --hash=sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3 \ 100 | --hash=sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb \ 101 | --hash=sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0 \ 102 | --hash=sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043 \ 103 | --hash=sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557 \ 104 | --hash=sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e \ 105 | --hash=sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21 \ 106 | --hash=sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d \ 107 | --hash=sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd \ 108 | --hash=sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0 \ 109 | --hash=sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337 \ 110 | --hash=sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753 \ 111 | --hash=sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804 \ 112 | --hash=sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f \ 113 | --hash=sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f \ 114 | --hash=sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5 \ 115 | --hash=sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5 \ 116 | --hash=sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1 \ 117 | --hash=sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00 \ 118 | --hash=sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2 \ 119 | --hash=sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050 \ 120 | --hash=sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e \ 121 | --hash=sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4 \ 122 | --hash=sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8 \ 123 | --hash=sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996 \ 124 | --hash=sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6 \ 125 | --hash=sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1 \ 126 | --hash=sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f \ 127 | --hash=sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1 \ 128 | --hash=sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4 \ 129 | --hash=sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b \ 130 | --hash=sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88 \ 131 | --hash=sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518 \ 132 | --hash=sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5 \ 133 | --hash=sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770 \ 134 | --hash=sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4 \ 135 | --hash=sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a \ 136 | --hash=sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76 \ 137 | --hash=sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe \ 138 | --hash=sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988 \ 139 | --hash=sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1 \ 140 | --hash=sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5 \ 141 | --hash=sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b \ 142 | --hash=sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7 \ 143 | --hash=sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8 \ 144 | --hash=sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc \ 145 | --hash=sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a \ 146 | --hash=sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720 \ 147 | --hash=sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3 \ 148 | --hash=sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b \ 149 | --hash=sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9 \ 150 | --hash=sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1 \ 151 | --hash=sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746 152 | # via python-jsonrpc-server 153 | -------------------------------------------------------------------------------- /src/test/python_tests/test_data/sample1/sample.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | print(x) 4 | -------------------------------------------------------------------------------- /src/test/python_tests/test_data/sample1/sample.unformatted: -------------------------------------------------------------------------------- 1 | import sys;print(x) -------------------------------------------------------------------------------- /src/test/python_tests/test_server.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """ 4 | Test for linting over LSP. 5 | """ 6 | 7 | from threading import Event 8 | 9 | from hamcrest import assert_that, is_ 10 | 11 | from .lsp_test_client import constants, defaults, session, utils 12 | 13 | TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.py" 14 | TEST_FILE_URI = utils.as_uri(str(TEST_FILE_PATH)) 15 | SERVER_INFO = utils.get_server_info_defaults() 16 | TIMEOUT = 10 # 10 seconds 17 | 18 | 19 | def test_linting_example(): 20 | """Test to linting on file open.""" 21 | contents = TEST_FILE_PATH.read_text() 22 | 23 | actual = [] 24 | with session.LspSession() as ls_session: 25 | ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) 26 | 27 | done = Event() 28 | 29 | def _handler(params): 30 | nonlocal actual 31 | actual = params 32 | done.set() 33 | 34 | ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) 35 | 36 | ls_session.notify_did_open( 37 | { 38 | "textDocument": { 39 | "uri": TEST_FILE_URI, 40 | "languageId": "python", 41 | "version": 1, 42 | "text": contents, 43 | } 44 | } 45 | ) 46 | 47 | # wait for some time to receive all notifications 48 | done.wait(TIMEOUT) 49 | 50 | # TODO: Add your linter specific diagnostic result here 51 | expected = { 52 | "uri": TEST_FILE_URI, 53 | "diagnostics": [ 54 | { 55 | # "range": { 56 | # "start": {"line": 0, "character": 0}, 57 | # "end": {"line": 0, "character": 0}, 58 | # }, 59 | # "message": "Missing module docstring", 60 | # "severity": 3, 61 | # "code": "C0114:missing-module-docstring", 62 | "source": SERVER_INFO["name"], 63 | }, 64 | { 65 | # "range": { 66 | # "start": {"line": 2, "character": 6}, 67 | # "end": { 68 | # "line": 2, 69 | # "character": 7, 70 | # }, 71 | # }, 72 | # "message": "Undefined variable 'x'", 73 | # "severity": 1, 74 | # "code": "E0602:undefined-variable", 75 | "source": SERVER_INFO["name"], 76 | }, 77 | { 78 | # "range": { 79 | # "start": {"line": 0, "character": 0}, 80 | # "end": { 81 | # "line": 0, 82 | # "character": 10, 83 | # }, 84 | # }, 85 | # "message": "Unused import sys", 86 | # "severity": 2, 87 | # "code": "W0611:unused-import", 88 | "source": SERVER_INFO["name"], 89 | }, 90 | ], 91 | } 92 | 93 | assert_that(actual, is_(expected)) 94 | 95 | 96 | def test_formatting_example(): 97 | """Test formatting a python file.""" 98 | FORMATTED_TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.py" 99 | UNFORMATTED_TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.unformatted" 100 | 101 | contents = UNFORMATTED_TEST_FILE_PATH.read_text() 102 | lines = contents.splitlines(keepends=False) 103 | 104 | actual = [] 105 | with utils.PythonFile(contents, UNFORMATTED_TEST_FILE_PATH.parent) as pf: 106 | uri = utils.as_uri(str(pf.fullpath)) 107 | 108 | with session.LspSession() as ls_session: 109 | ls_session.initialize() 110 | ls_session.notify_did_open( 111 | { 112 | "textDocument": { 113 | "uri": uri, 114 | "languageId": "python", 115 | "version": 1, 116 | "text": contents, 117 | } 118 | } 119 | ) 120 | actual = ls_session.text_document_formatting( 121 | { 122 | "textDocument": {"uri": uri}, 123 | # `options` is not used by black 124 | "options": {"tabSize": 4, "insertSpaces": True}, 125 | } 126 | ) 127 | 128 | expected = [ 129 | { 130 | "range": { 131 | "start": {"line": 0, "character": 0}, 132 | "end": {"line": len(lines), "character": 0}, 133 | }, 134 | "newText": FORMATTED_TEST_FILE_PATH.read_text(), 135 | } 136 | ] 137 | 138 | assert_that(actual, is_(expected)) 139 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": ["ES2020"], 6 | "sourceMap": true, 7 | "rootDir": "src", 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUnusedParameters": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2', 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'], 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader', 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | devtool: 'source-map', 44 | infrastructureLogging: { 45 | level: 'log', // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [extensionConfig]; 49 | --------------------------------------------------------------------------------