├── .coveragerc ├── .github └── workflows │ ├── release.yaml │ └── testing.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── example-initialization-request.txt ├── instance └── .gitignore ├── jedi_language_server ├── __init__.py ├── cli.py ├── constants.py ├── initialization_options.py ├── jedi_utils.py ├── notebook_utils.py ├── py.typed ├── pygls_utils.py ├── server.py ├── text_edit_utils.py └── type_map.py ├── noxfile.py ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── lsp_test_client ├── __init__.py ├── defaults.py ├── lsp_run.py ├── session.py └── utils.py ├── lsp_tests ├── test_completion.py ├── test_definition.py ├── test_diagnostics.py ├── test_document_symbol.py ├── test_highlighting.py ├── test_hover.py ├── test_initialize.py ├── test_refactoring.py ├── test_references.py ├── test_semantic_tokens.py ├── test_signature.py └── test_workspace_symbol.py ├── test_cli.py ├── test_data ├── completion │ ├── completion_test1.ipynb │ ├── completion_test1.py │ ├── completion_test2.py │ └── completion_test_class_self.py ├── definition │ ├── definition_test1.ipynb │ ├── definition_test1.py │ ├── definition_test2.ipynb │ ├── somemodule.py │ └── somemodule2.py ├── diagnostics │ ├── diagnostics_test1_content_changes.json │ └── diagnostics_test1_contents.txt ├── highlighting │ ├── highlighting_test1.ipynb │ └── highlighting_test1.py ├── hover │ ├── hover_test1.ipynb │ ├── hover_test1.py │ └── somemodule.py ├── refactoring │ ├── code_action_test1.ipynb │ ├── code_action_test1.py │ ├── code_action_test2.py │ ├── rename_module.py │ ├── rename_package_test1 │ │ ├── old_name │ │ │ ├── __init__.py │ │ │ └── some_module.py │ │ ├── rename_test_main.ipynb │ │ └── rename_test_main.py │ ├── rename_test1.ipynb │ ├── rename_test1.py │ ├── rename_test2.py │ ├── rename_test3.py │ └── somepackage │ │ ├── __init__.py │ │ └── somemodule.py ├── references │ ├── references_test1.ipynb │ └── references_test1.py ├── semantic_tokens │ ├── semantic_tokens_test1.py │ └── semantic_tokens_test2.py ├── signature │ ├── signature_test1.ipynb │ └── signature_test1.py └── symbol │ ├── somemodule.py │ ├── somemodule2.py │ ├── symbol_test1.ipynb │ └── symbol_test1.py ├── test_debounce.py └── test_initialization_options.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | parallel = True 4 | data_file = coverage/.coverage 5 | include = 6 | src/jedi_language_server/* 7 | 8 | [report] 9 | exclude_lines = 10 | # Asserts and error conditions. 11 | raise AssertionError 12 | raise NotImplementedError 13 | 14 | 15 | [html] 16 | directory = coverage/html 17 | title = debugpy coverage report 18 | 19 | [xml] 20 | output = coverage/coverage.xml 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Setup Python 11 | uses: actions/setup-python@v5 12 | with: 13 | python-version: "3.12" 14 | architecture: x64 15 | - name: Deploy 16 | env: 17 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }} 18 | run: | 19 | python -m pip install -U pip 20 | python -m pip install wheel 21 | python -m pip install poetry 22 | poetry build 23 | poetry publish 24 | -------------------------------------------------------------------------------- /.github/workflows/testing.yaml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Select Python 3.12 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.12" 17 | architecture: x64 18 | - name: Install Dependencies 19 | run: | 20 | python -m pip install -U pip 21 | python -m pip install wheel 22 | python -m pip install poetry 23 | poetry install 24 | - name: Run linting 25 | run: poetry run nox -s lint 26 | - name: Run static type checking 27 | run: poetry run nox -s typecheck 28 | tests: 29 | needs: [lint] 30 | runs-on: ${{ matrix.os }} 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest, windows-latest] 34 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | - name: Setup, Python ${{ matrix.python-version }} 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | architecture: x64 43 | - name: Install Dependencies 44 | run: | 45 | python -m pip install -U pip 46 | python -m pip install wheel 47 | python -m pip install poetry 48 | poetry install 49 | - name: Run Tests 50 | run: poetry run nox -s tests 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | # Python.gitignore 3 | ################################################################# 4 | 5 | # My local dev stuff 6 | test.py 7 | .nvimrc 8 | .vim 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | coverage/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | 72 | # Flask stuff: 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # celery beat schedule file 102 | celerybeat-schedule 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .setup_complete 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # VS Code 136 | .vscode/ 137 | 138 | # VS 139 | .vs/ 140 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: lint 6 | name: lint 7 | entry: poetry run nox -s lint 8 | language: system 9 | types: [python] 10 | pass_filenames: false 11 | exclude: ^tests/test_data/ 12 | - repo: local 13 | hooks: 14 | - id: typecheck 15 | name: typecheck 16 | entry: poetry run nox -s typecheck 17 | language: system 18 | types: [python] 19 | pass_filenames: false 20 | exclude: ^tests/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sam Roeca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: ## Print this help menu 3 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ 4 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 5 | 6 | .PHONY: require 7 | require: ## Check that prerequisites are installed. 8 | @if ! command -v python3 > /dev/null; then \ 9 | printf "\033[1m\033[31mERROR\033[0m: python3 not installed\n" >&2 ; \ 10 | exit 1; \ 11 | fi 12 | @if ! python3 -c "import sys; sys.exit(sys.version_info < (3,8))"; then \ 13 | printf "\033[1m\033[31mERROR\033[0m: python 3.8+ required\n" >&2 ; \ 14 | exit 1; \ 15 | fi 16 | @if ! command -v poetry > /dev/null; then \ 17 | printf "\033[1m\033[31mERROR\033[0m: poetry not installed.\n" >&2 ; \ 18 | printf "Please install with 'python3 -mpip install --user poetry'\n" >&2 ; \ 19 | exit 1; \ 20 | fi 21 | 22 | .PHONY: setup 23 | setup: require .setup_complete ## Set up the local development environment 24 | 25 | .setup_complete: poetry.lock ## Internal helper to run the setup. 26 | poetry install 27 | poetry run pre-commit install 28 | touch .setup_complete 29 | 30 | .PHONY: fix 31 | fix: ## Fix all files in-place 32 | poetry run nox -s $@ 33 | 34 | .PHONY: lint 35 | lint: ## Run linters on all files 36 | poetry run nox -s $@ 37 | 38 | .PHONY: typecheck 39 | typecheck: ## Run static type checks 40 | poetry run nox -s $@ 41 | 42 | .PHONY: tests 43 | tests: ## Run unit tests 44 | poetry run nox -s $@ 45 | 46 | .PHONY: publish 47 | publish: ## Build & publish the new version 48 | poetry build 49 | poetry publish 50 | 51 | .PHONY: clean 52 | clean: ## Remove local development environment 53 | if poetry env list | grep -q Activated; then \ 54 | poetry env remove python3; \ 55 | fi 56 | rm -f .setup_complete 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jedi Language Server 2 | 3 | [![image-version](https://img.shields.io/pypi/v/jedi-language-server.svg)](https://python.org/pypi/jedi-language-server) 4 | [![image-license](https://img.shields.io/pypi/l/jedi-language-server.svg)](https://python.org/pypi/jedi-language-server) 5 | [![image-python-versions](https://img.shields.io/badge/python->=3.9-blue)](https://python.org/pypi/jedi-language-server) 6 | [![image-pypi-downloads](https://static.pepy.tech/badge/jedi-language-server)](https://pepy.tech/projects/jedi-language-server) 7 | [![github-action-testing](https://github.com/pappasam/jedi-language-server/actions/workflows/testing.yaml/badge.svg)](https://github.com/pappasam/jedi-language-server/actions/workflows/testing.yaml) 8 | [![poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) 9 | 10 | A [Python](https://www.python.org/) [Language Server](https://microsoft.github.io/language-server-protocol/), with additional support for [computational notebooks](https://docs.jupyter.org/en/latest/#what-is-a-notebook), powered by the latest version of [Jedi](https://jedi.readthedocs.io/en/latest/). 11 | 12 | ## Installation 13 | 14 | Some frameworks, like coc-jedi and vscode-python, will install and manage `jedi-language-server` for you. If you're setting up manually, you can run the following from your command line (bash / zsh): 15 | 16 | ```bash 17 | pip install -U jedi-language-server 18 | ``` 19 | 20 | Alternatively (and preferably), use [pipx](https://github.com/pipxproject/pipx) to keep `jedi-language-server` and its dependencies isolated from your other Python dependencies. Don't worry, jedi is smart enough to figure out which Virtual environment you're currently using! 21 | 22 | ## Editor Setup 23 | 24 | The following instructions show how to use `jedi-language-server` with your development tooling. The instructions assume you have already installed `jedi-language-server`. 25 | 26 | ### Vim and Neovim 27 | 28 | For Neovim, this project is supported out-of-the-box by [Neovim's native LSP client](https://neovim.io/doc/user/lsp.html) through [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig). See [here](https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#jedi_language_server) for an example configuration. 29 | 30 | For Vim, here are some additional, actively maintained options: 31 | 32 | - [ALE](https://github.com/dense-analysis/ale). 33 | - [vim-lsp](https://github.com/prabirshrestha/vim-lsp). 34 | 35 | Note: this list is non-exhaustive. If you know of a great choice not included in this list, please submit a PR! 36 | 37 | ### Emacs 38 | 39 | Users may choose one of the following options: 40 | 41 | - [lsp-jedi](https://github.com/fredcamps/lsp-jedi). 42 | - [eglot](https://github.com/joaotavora/eglot). 43 | - [lsp-bridge](https://github.com/manateelazycat/lsp-bridge). 44 | - [lspce](https://github.com/zbelial/lspce). 45 | 46 | Note: this list is non-exhaustive. If you know of a great choice not included in this list, please submit a PR! 47 | 48 | ### Visual Studio Code (vscode) 49 | 50 | Starting from the [October 2021 release](https://github.com/microsoft/vscode-python/releases/tag/2021.10.1317843341), set the `python.languageServer` setting to `Jedi` to use `jedi-language-server`. 51 | 52 | See: 53 | 54 | ## Configuration 55 | 56 | `jedi-language-server` supports the following [initializationOptions](https://microsoft.github.io/language-server-protocol/specification#initialize): 57 | 58 | ```json 59 | { 60 | "initializationOptions": { 61 | "codeAction": { 62 | "nameExtractVariable": "jls_extract_var", 63 | "nameExtractFunction": "jls_extract_def" 64 | }, 65 | "completion": { 66 | "disableSnippets": false, 67 | "resolveEagerly": false, 68 | "ignorePatterns": [] 69 | }, 70 | "diagnostics": { 71 | "enable": false, 72 | "didOpen": true, 73 | "didChange": true, 74 | "didSave": true 75 | }, 76 | "hover": { 77 | "enable": true, 78 | "disable": { 79 | "class": { "all": false, "names": [], "fullNames": [] }, 80 | "function": { "all": false, "names": [], "fullNames": [] }, 81 | "instance": { "all": false, "names": [], "fullNames": [] }, 82 | "keyword": { "all": false, "names": [], "fullNames": [] }, 83 | "module": { "all": false, "names": [], "fullNames": [] }, 84 | "param": { "all": false, "names": [], "fullNames": [] }, 85 | "path": { "all": false, "names": [], "fullNames": [] }, 86 | "property": { "all": false, "names": [], "fullNames": [] }, 87 | "statement": { "all": false, "names": [], "fullNames": [] } 88 | } 89 | }, 90 | "jediSettings": { 91 | "autoImportModules": [], 92 | "caseInsensitiveCompletion": true, 93 | "debug": false 94 | }, 95 | "markupKindPreferred": "markdown", 96 | "workspace": { 97 | "extraPaths": [], 98 | "environmentPath": "/path/to/venv/bin/python", 99 | "symbols": { 100 | "ignoreFolders": [".nox", ".tox", ".venv", "__pycache__", "venv"], 101 | "maxSymbols": 20 102 | } 103 | }, 104 | "semanticTokens": { 105 | "enable": false 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | The different sections of the InitializationOptions are explained below, in detail. Section headers use a `.` to separate nested JSON-object keys. 112 | 113 | ### markupKindPreferred 114 | 115 | The preferred MarkupKind for all `jedi-language-server` messages that take [MarkupContent](https://microsoft.github.io/language-server-protocol/specification#markupContent). 116 | 117 | - type: `string` 118 | - accepted values: `"markdown"`, `"plaintext"` 119 | 120 | If omitted, `jedi-language-server` defaults to the client-preferred configuration. If there is no client-preferred configuration, jedi language server users `"plaintext"`. 121 | 122 | ### jediSettings.autoImportModules 123 | 124 | Modules that jedi will directly import without analyzing. Improves autocompletion but loses goto definition. 125 | 126 | - type: `string[]` 127 | - default: `[]` 128 | 129 | If you're noticing that modules like `numpy` and `pandas` are taking a super long time to load, and you value completions / signatures over goto definition, I recommend using this option like this: 130 | 131 | ```json 132 | { 133 | "jediSettings": { 134 | "autoImportModules": ["numpy", "pandas"] 135 | } 136 | } 137 | ``` 138 | 139 | ### jediSettings.caseInsensitiveCompletion 140 | 141 | Completions are by default case-insensitive. Set to `false` to make completions case-sensitive. 142 | 143 | - type: `boolean` 144 | - default: `true` 145 | 146 | ```json 147 | { 148 | "jediSettings": { 149 | "caseInsensitiveCompletion": false 150 | } 151 | } 152 | ``` 153 | 154 | ### jediSettings.debug 155 | 156 | Print jedi debugging messages to stderr. 157 | 158 | - type: `boolean` 159 | - default: `false` 160 | 161 | ```json 162 | { 163 | "jediSettings": { 164 | "debug": false 165 | } 166 | } 167 | ``` 168 | 169 | ### codeAction.nameExtractFunction 170 | 171 | Function name generated by the 'extract_function' codeAction. 172 | 173 | - type: `string` 174 | - default: `"jls_extract_def"` 175 | 176 | ### codeAction.nameExtractVariable 177 | 178 | Variable name generated by the 'extract_variable' codeAction. 179 | 180 | - type: `string` 181 | - default: `"jls_extract_var"` 182 | 183 | ### completion.disableSnippets 184 | 185 | If your language client supports `CompletionItem` snippets but you don't like them, disable them by setting this option to `true`. 186 | 187 | - type: `boolean` 188 | - default: `false` 189 | 190 | ### completion.resolveEagerly 191 | 192 | Return all completion results in initial completion request. Set to `true` if your language client does not support `completionItem/resolve`. 193 | 194 | - type: `boolean` 195 | - default: `false` 196 | 197 | ### completion.ignorePatterns 198 | 199 | A list of regular expressions. If any regular expression in ignorePatterns matches a completion's name, that completion item is not returned to the client. 200 | 201 | - type: `string[]` 202 | - default: `[]` 203 | 204 | In general, you should prefer the default value for this option. Jedi is good at filtering values for end users. That said, there are situations where IDE developers, or some programmers in some codebases, may want to filter some completions by name. This flexible interface is provided to accommodate these advanced use cases. If you have one of these advanced use cases, see below for some example patterns (and their corresponding regular expression). 205 | 206 | #### All Private Names 207 | 208 | | Matches | Non-Matches | 209 | | ------------------- | ------------ | 210 | | `_hello`, `__world` | `__dunder__` | 211 | 212 | Regular Expression: 213 | 214 | ```re 215 | ^_{1,3}$|^_[^_].*$|^__.*(?`, which will insert a `^M`. See: 454 | 455 | ```console 456 | $ jedi-language-server 2>logs 457 | Content-Length: 1062^M 458 | ^M 459 | ... 460 | ``` 461 | 462 | ## Technical capabilities 463 | 464 | jedi-language-server aims to support Jedi's capabilities and expose them through the Language Server Protocol. It supports the following Language Server capabilities: 465 | 466 | ### Language Features 467 | 468 | - [completionItem/resolve](https://microsoft.github.io/language-server-protocol/specification#completionItem_resolve) 469 | - [textDocument/codeAction](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction) (refactor.inline, refactor.extract) 470 | - [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion) 471 | - [textDocument/declaration](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration) 472 | - [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition) 473 | - [textDocument/documentHighlight](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentHighlight) 474 | - [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol) 475 | - [textDocument/typeDefinition](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition) 476 | - [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover) 477 | - [textDocument/publishDiagnostics](https://microsoft.github.io/language-server-protocol/specification#textDocument_publishDiagnostics) 478 | - [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references) 479 | - [textDocument/rename](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename) 480 | - [textDocument/semanticTokens](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) _(under development)_ 481 | - [textDocument/signatureHelp](https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp) 482 | - [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol) 483 | 484 | ### Text Synchronization (for diagnostics) 485 | 486 | - [textDocument/didChange](https://microsoft.github.io/language-server-protocol/specification#textDocument_didChange) 487 | - [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specification#textDocument_didOpen) 488 | - [textDocument/didSave](https://microsoft.github.io/language-server-protocol/specification#textDocument_didSave) 489 | 490 | ### Notebook document support 491 | 492 | - [NotebookDocumentSyncClientCapabilities](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notebookDocumentSyncClientCapabilities) 493 | 494 | ## Local Development 495 | 496 | To build and run this project from source: 497 | 498 | ### Dependencies 499 | 500 | Install the following tools manually: 501 | 502 | - [Poetry](https://github.com/sdispater/poetry#installation) 503 | - [GNU Make](https://www.gnu.org/software/make/) 504 | 505 | #### Recommended 506 | 507 | - [asdf](https://github.com/asdf-vm/asdf) 508 | 509 | ### Get source code 510 | 511 | [Fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) this repository and clone the fork to your development machine: 512 | 513 | ```bash 514 | git clone https://github.com//jedi-language-server 515 | cd jedi-language-server 516 | ``` 517 | 518 | ### Set up development environment 519 | 520 | ```bash 521 | make setup 522 | ``` 523 | 524 | ### Automatically format files 525 | 526 | ```bash 527 | make fix 528 | ``` 529 | 530 | ### Run tests 531 | 532 | ```bash 533 | make lint 534 | make typecheck 535 | make tests 536 | ``` 537 | 538 | ## Inspiration 539 | 540 | Palantir's [python-language-server](https://github.com/palantir/python-language-server) inspired this project. In fact, for consistency's sake, many of python-language-server's CLI options are used as-is in `jedi-language-server`. 541 | 542 | Unlike python-language-server, `jedi-language-server`: 543 | 544 | - Uses [pygls](https://github.com/openlawlibrary/pygls) instead of creating its own low-level Language Server Protocol bindings 545 | - Supports one powerful 3rd party static analysis / completion / refactoring library: Jedi. By only supporting Jedi, we can focus on supporting all Jedi features without exposing ourselves to too many broken 3rd party dependencies. 546 | - Is supremely simple because of its scope constraints. Leave complexity to the Jedi [master](https://github.com/davidhalter). If the force is strong with you, please submit a PR! 547 | 548 | ## Articles 549 | 550 | - [Python in VS Code Improves Jedi Language Server Support](https://visualstudiomagazine.com/articles/2021/03/17/vscode-jedi.aspx) 551 | 552 | ## Written by 553 | 554 | [Samuel Roeca](https://samroeca.com/) 555 | -------------------------------------------------------------------------------- /example-initialization-request.txt: -------------------------------------------------------------------------------- 1 | Content-Length: 1062 2 | 3 | {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"capabilities":{"textDocument":{"hover":{"dynamicRegistration":true,"contentFormat":["plaintext","markdown"]},"synchronization":{"dynamicRegistration":true,"willSave":false,"didSave":false,"willSaveWaitUntil":false},"completion":{"dynamicRegistration":true,"completionItem":{"snippetSupport":false,"commitCharactersSupport":true,"documentationFormat":["plaintext","markdown"],"deprecatedSupport":false,"preselectSupport":false},"contextSupport":false},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["plaintext","markdown"]}},"declaration":{"dynamicRegistration":true,"linkSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true}},"workspace":{"didChangeConfiguration":{"dynamicRegistration":true}}},"initializationOptions":null,"processId":null,"rootUri":"file:///home/ubuntu/artifacts/","workspaceFolders":null}} 4 | -------------------------------------------------------------------------------- /instance/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /jedi_language_server/__init__.py: -------------------------------------------------------------------------------- 1 | """Jedi Language Server.""" 2 | 3 | from importlib.metadata import version 4 | 5 | __version__ = version("jedi-language-server") 6 | -------------------------------------------------------------------------------- /jedi_language_server/cli.py: -------------------------------------------------------------------------------- 1 | """Jedi Language Server command line interface.""" 2 | 3 | import argparse 4 | import logging 5 | import sys 6 | 7 | from . import __version__ 8 | from .server import SERVER 9 | 10 | 11 | def get_version() -> str: 12 | """Get the program version.""" 13 | return __version__ 14 | 15 | 16 | def cli() -> None: 17 | """Jedi language server cli entrypoint.""" 18 | parser = argparse.ArgumentParser( 19 | prog="jedi-language-server", 20 | formatter_class=argparse.RawDescriptionHelpFormatter, 21 | description="Jedi language server: an LSP wrapper for jedi.", 22 | epilog="""\ 23 | Examples: 24 | 25 | Run over stdio : jedi-language-server 26 | Run over tcp : jedi-language-server --tcp 27 | Run over websockets: 28 | # only need to pip install once per env 29 | pip install pygls[ws] 30 | jedi-language-server --ws 31 | 32 | Notes: 33 | 34 | For use with web sockets, user must first run 35 | 'pip install pygls[ws]' to install the correct 36 | version of the websockets library. 37 | """, 38 | ) 39 | parser.add_argument( 40 | "--version", 41 | help="display version information and exit", 42 | action="store_true", 43 | ) 44 | parser.add_argument( 45 | "--tcp", 46 | help="use TCP web server instead of stdio", 47 | action="store_true", 48 | ) 49 | parser.add_argument( 50 | "--ws", 51 | help="use web socket server instead of stdio", 52 | action="store_true", 53 | ) 54 | parser.add_argument( 55 | "--host", 56 | help="host for web server (default 127.0.0.1)", 57 | type=str, 58 | default="127.0.0.1", 59 | ) 60 | parser.add_argument( 61 | "--port", 62 | help="port for web server (default 2087)", 63 | type=int, 64 | default=2087, 65 | ) 66 | parser.add_argument( 67 | "--log-file", 68 | help="redirect logs to file specified", 69 | type=str, 70 | ) 71 | parser.add_argument( 72 | "-v", 73 | "--verbose", 74 | help="increase verbosity of log output", 75 | action="count", 76 | default=0, 77 | ) 78 | args = parser.parse_args() 79 | if args.version: 80 | print(get_version()) 81 | sys.exit(0) 82 | if args.tcp and args.ws: 83 | print( 84 | "Error: --tcp and --ws cannot both be specified", 85 | file=sys.stderr, 86 | ) 87 | sys.exit(1) 88 | log_level = {0: logging.WARN, 1: logging.INFO, 2: logging.DEBUG}.get( 89 | args.verbose, 90 | logging.DEBUG, 91 | ) 92 | 93 | if args.log_file: 94 | logging.basicConfig( 95 | filename=args.log_file, 96 | filemode="w", 97 | level=log_level, 98 | ) 99 | else: 100 | logging.basicConfig(stream=sys.stderr, level=log_level) 101 | 102 | if args.tcp: 103 | SERVER.start_tcp(host=args.host, port=args.port) 104 | elif args.ws: 105 | SERVER.start_ws(host=args.host, port=args.port) 106 | else: 107 | SERVER.start_io() 108 | -------------------------------------------------------------------------------- /jedi_language_server/constants.py: -------------------------------------------------------------------------------- 1 | """Constants.""" 2 | 3 | from lsprotocol.types import SemanticTokenTypes 4 | 5 | MAX_CONCURRENT_DEBOUNCE_CALLS = 10 6 | """The maximum number of concurrent calls allowed by the debounce decorator.""" 7 | 8 | SEMANTIC_TO_TOKEN_TYPE = { 9 | "module": SemanticTokenTypes.Namespace, 10 | "class": SemanticTokenTypes.Class, 11 | "function": SemanticTokenTypes.Function, 12 | "param": SemanticTokenTypes.Parameter, 13 | "statement": SemanticTokenTypes.Variable, 14 | "property": SemanticTokenTypes.Property, 15 | } 16 | 17 | SUPPORTED_SEMANTIC_TYPES = [t.value for t in SEMANTIC_TO_TOKEN_TYPE.values()] 18 | 19 | SEMANTIC_TO_TOKEN_ID = { 20 | key: index for index, key in enumerate(SEMANTIC_TO_TOKEN_TYPE.keys()) 21 | } 22 | -------------------------------------------------------------------------------- /jedi_language_server/initialization_options.py: -------------------------------------------------------------------------------- 1 | """Module containing the InitializationOptions parser. 2 | 3 | Provides a fully defaulted pydantic model for this language server's 4 | initialization options. 5 | """ 6 | 7 | import re 8 | import sys 9 | from dataclasses import dataclass, field, fields, is_dataclass 10 | from typing import Any, List, Optional, Pattern, Set 11 | 12 | from cattrs import Converter 13 | from cattrs.gen import make_dict_structure_fn, override 14 | from lsprotocol.types import MarkupKind 15 | 16 | if sys.version_info >= (3, 10): 17 | light_dataclass = dataclass(kw_only=True, eq=False, match_args=False) 18 | else: 19 | light_dataclass = dataclass(eq=False) 20 | 21 | 22 | @light_dataclass 23 | class CodeAction: 24 | name_extract_variable: str = "jls_extract_var" 25 | name_extract_function: str = "jls_extract_def" 26 | 27 | 28 | @light_dataclass 29 | class Completion: 30 | disable_snippets: bool = False 31 | resolve_eagerly: bool = False 32 | ignore_patterns: List[Pattern[str]] = field(default_factory=list) 33 | 34 | 35 | @light_dataclass 36 | class Diagnostics: 37 | enable: bool = True 38 | did_open: bool = True 39 | did_save: bool = True 40 | did_change: bool = True 41 | 42 | 43 | @light_dataclass 44 | class HoverDisableOptions: 45 | all: bool = False 46 | names: Set[str] = field(default_factory=set) 47 | full_names: Set[str] = field(default_factory=set) 48 | 49 | 50 | @light_dataclass 51 | class HoverDisable: 52 | """All Attributes have _ appended to avoid syntax conflicts. 53 | 54 | For example, the keyword class would have required a special case. 55 | To get around this, I decided it's simpler to always assume an 56 | underscore at the end. 57 | """ 58 | 59 | keyword_: HoverDisableOptions = field(default_factory=HoverDisableOptions) 60 | module_: HoverDisableOptions = field(default_factory=HoverDisableOptions) 61 | class_: HoverDisableOptions = field(default_factory=HoverDisableOptions) 62 | instance_: HoverDisableOptions = field(default_factory=HoverDisableOptions) 63 | function_: HoverDisableOptions = field(default_factory=HoverDisableOptions) 64 | param_: HoverDisableOptions = field(default_factory=HoverDisableOptions) 65 | path_: HoverDisableOptions = field(default_factory=HoverDisableOptions) 66 | property_: HoverDisableOptions = field(default_factory=HoverDisableOptions) 67 | statement_: HoverDisableOptions = field( 68 | default_factory=HoverDisableOptions 69 | ) 70 | 71 | 72 | @light_dataclass 73 | class Hover: 74 | enable: bool = True 75 | disable: HoverDisable = field(default_factory=HoverDisable) 76 | 77 | 78 | @light_dataclass 79 | class JediSettings: 80 | auto_import_modules: List[str] = field(default_factory=list) 81 | case_insensitive_completion: bool = True 82 | debug: bool = False 83 | 84 | 85 | @light_dataclass 86 | class Symbols: 87 | ignore_folders: List[str] = field( 88 | default_factory=lambda: [".nox", ".tox", ".venv", "__pycache__"] 89 | ) 90 | max_symbols: int = 20 91 | 92 | 93 | @light_dataclass 94 | class Workspace: 95 | environment_path: Optional[str] = None 96 | extra_paths: List[str] = field(default_factory=list) 97 | symbols: Symbols = field(default_factory=Symbols) 98 | 99 | 100 | @light_dataclass 101 | class SemanticTokens: 102 | enable: bool = False 103 | 104 | 105 | @light_dataclass 106 | class InitializationOptions: 107 | code_action: CodeAction = field(default_factory=CodeAction) 108 | completion: Completion = field(default_factory=Completion) 109 | diagnostics: Diagnostics = field(default_factory=Diagnostics) 110 | hover: Hover = field(default_factory=Hover) 111 | jedi_settings: JediSettings = field(default_factory=JediSettings) 112 | markup_kind_preferred: Optional[MarkupKind] = None 113 | workspace: Workspace = field(default_factory=Workspace) 114 | semantic_tokens: SemanticTokens = field(default_factory=SemanticTokens) 115 | 116 | 117 | initialization_options_converter = Converter() 118 | 119 | WEIRD_NAMES = { 120 | "keyword_": "keyword", 121 | "module_": "module", 122 | "class_": "class", 123 | "instance_": "instance", 124 | "function_": "function", 125 | "param_": "param", 126 | "path_": "path", 127 | "property_": "property", 128 | "statement_ ": "statement", 129 | } 130 | 131 | 132 | def convert_class_keys(string: str) -> str: 133 | """Convert from snake_case to camelCase. 134 | 135 | Also handles random special cases for keywords. 136 | """ 137 | if string in WEIRD_NAMES: 138 | return WEIRD_NAMES[string] 139 | return "".join( 140 | word.capitalize() if idx > 0 else word 141 | for idx, word in enumerate(string.split("_")) 142 | ) 143 | 144 | 145 | def structure(cls: type) -> Any: 146 | """Hook to convert names when marshalling initialization_options.""" 147 | return make_dict_structure_fn( 148 | cls, 149 | initialization_options_converter, 150 | **{ # type: ignore[arg-type] 151 | a.name: override(rename=convert_class_keys(a.name)) 152 | for a in fields(cls) 153 | }, 154 | ) 155 | 156 | 157 | initialization_options_converter.register_structure_hook_factory( 158 | is_dataclass, structure 159 | ) 160 | 161 | 162 | initialization_options_converter.register_structure_hook_factory( 163 | lambda x: x == Pattern[str], 164 | lambda _: lambda x, _: re.compile(x), 165 | ) 166 | 167 | initialization_options_converter.register_unstructure_hook_factory( 168 | lambda x: x == Pattern[str], 169 | lambda _: lambda x: x.pattern, 170 | ) 171 | -------------------------------------------------------------------------------- /jedi_language_server/notebook_utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions for handling notebook documents.""" 2 | 3 | from collections import defaultdict 4 | from typing import ( 5 | Any, 6 | Callable, 7 | Dict, 8 | Iterable, 9 | List, 10 | NamedTuple, 11 | Optional, 12 | TypeVar, 13 | Union, 14 | cast, 15 | ) 16 | 17 | import attrs 18 | from lsprotocol.types import ( 19 | AnnotatedTextEdit, 20 | CallHierarchyPrepareParams, 21 | CodeActionParams, 22 | ColorPresentationParams, 23 | CompletionParams, 24 | DefinitionParams, 25 | DocumentHighlightParams, 26 | DocumentOnTypeFormattingParams, 27 | Hover, 28 | HoverParams, 29 | InlayHintParams, 30 | InlineValueParams, 31 | Location, 32 | NotebookDocument, 33 | OptionalVersionedTextDocumentIdentifier, 34 | Position, 35 | PrepareRenameParams, 36 | Range, 37 | ReferenceParams, 38 | RenameParams, 39 | SemanticTokensRangeParams, 40 | SignatureHelpParams, 41 | TextDocumentEdit, 42 | TextDocumentPositionParams, 43 | TextEdit, 44 | ) 45 | from pygls.server import LanguageServer 46 | from pygls.workspace import TextDocument, Workspace 47 | 48 | 49 | def notebook_coordinate_mapper( 50 | workspace: Workspace, 51 | *, 52 | notebook_uri: Optional[str] = None, 53 | cell_uri: Optional[str] = None, 54 | ) -> Optional["NotebookCoordinateMapper"]: 55 | notebook_document = workspace.get_notebook_document( 56 | notebook_uri=notebook_uri, cell_uri=cell_uri 57 | ) 58 | if notebook_document is None: 59 | return None 60 | cells = [ 61 | workspace.text_documents[cell.document] 62 | for cell in notebook_document.cells 63 | ] 64 | return NotebookCoordinateMapper(notebook_document, cells) 65 | 66 | 67 | class DocumentPosition(NamedTuple): 68 | """A position in a document.""" 69 | 70 | uri: str 71 | position: Position 72 | 73 | 74 | class DocumentTextEdit(NamedTuple): 75 | """A text edit in a document.""" 76 | 77 | uri: str 78 | text_edit: Union[TextEdit, AnnotatedTextEdit] 79 | 80 | 81 | class NotebookCoordinateMapper: 82 | """Maps positions between individual notebook cells and the concatenated notebook document.""" 83 | 84 | def __init__( 85 | self, 86 | notebook_document: NotebookDocument, 87 | cells: List[TextDocument], 88 | ): 89 | self._document = notebook_document 90 | self._cells = cells 91 | 92 | # Construct helper data structures. 93 | self._cell_by_uri: Dict[str, TextDocument] = {} 94 | self._cell_line_range_by_uri: Dict[str, range] = {} 95 | start_line = 0 96 | for index, cell in enumerate(self._cells): 97 | end_line = start_line + len(cell.lines) 98 | 99 | self._cell_by_uri[cell.uri] = cell 100 | self._cell_line_range_by_uri[cell.uri] = range( 101 | start_line, end_line 102 | ) 103 | 104 | start_line = end_line 105 | 106 | @property 107 | def notebook_source(self) -> str: 108 | """Concatenated notebook source.""" 109 | return "\n".join(cell.source for cell in self._cells) 110 | 111 | @property 112 | def notebook_uri(self) -> str: 113 | """The notebook document's URI.""" 114 | return self._document.uri 115 | 116 | def notebook_position( 117 | self, cell_uri: str, cell_position: Position 118 | ) -> Position: 119 | """Convert a cell position to a concatenated notebook position.""" 120 | line = ( 121 | self._cell_line_range_by_uri[cell_uri].start + cell_position.line 122 | ) 123 | return Position(line=line, character=cell_position.character) 124 | 125 | def notebook_range(self, cell_uri: str, cell_range: Range) -> Range: 126 | """Convert a cell range to a concatenated notebook range.""" 127 | start = self.notebook_position(cell_uri, cell_range.start) 128 | end = self.notebook_position(cell_uri, cell_range.end) 129 | return Range(start=start, end=end) 130 | 131 | def cell_position( 132 | self, notebook_position: Position 133 | ) -> Optional[DocumentPosition]: 134 | """Convert a concatenated notebook position to a cell position.""" 135 | for cell in self._cells: 136 | line_range = self._cell_line_range_by_uri[cell.uri] 137 | if notebook_position.line in line_range: 138 | line = notebook_position.line - line_range.start 139 | return DocumentPosition( 140 | uri=cell.uri, 141 | position=Position( 142 | line=line, character=notebook_position.character 143 | ), 144 | ) 145 | return None 146 | 147 | def cell_range(self, notebook_range: Range) -> Optional[Location]: 148 | """Convert a concatenated notebook range to a cell range. 149 | 150 | Returns a `Location` to identify the cell that the range is in. 151 | """ 152 | start = self.cell_position(notebook_range.start) 153 | if start is None: 154 | return None 155 | 156 | end = self.cell_position(notebook_range.end) 157 | if end is None: 158 | return None 159 | 160 | if start.uri != end.uri: 161 | return None 162 | 163 | return Location( 164 | uri=start.uri, range=Range(start=start.position, end=end.position) 165 | ) 166 | 167 | def cell_location(self, notebook_location: Location) -> Optional[Location]: 168 | """Convert a concatenated notebook location to a cell location.""" 169 | if notebook_location.uri != self._document.uri: 170 | return None 171 | return self.cell_range(notebook_location.range) 172 | 173 | def cell_index(self, cell_uri: str) -> Optional[int]: 174 | """Get the index of a cell by its URI.""" 175 | for index, cell in enumerate(self._cells): 176 | if cell.uri == cell_uri: 177 | return index 178 | return None 179 | 180 | def cell_text_edit( 181 | self, text_edit: Union[TextEdit, AnnotatedTextEdit] 182 | ) -> Optional[DocumentTextEdit]: 183 | """Convert a concatenated notebook text edit to a cell text edit.""" 184 | location = self.cell_range(text_edit.range) 185 | if location is None: 186 | return None 187 | 188 | return DocumentTextEdit( 189 | uri=location.uri, 190 | text_edit=attrs.evolve(text_edit, range=location.range), 191 | ) 192 | 193 | def cell_text_document_edits( 194 | self, text_document_edit: TextDocumentEdit 195 | ) -> Iterable[TextDocumentEdit]: 196 | """Convert a concatenated notebook text document edit to cell text document edits.""" 197 | if text_document_edit.text_document.uri != self._document.uri: 198 | return 199 | 200 | # Convert edits in the concatenated notebook to per-cell edits, grouped by cell URI. 201 | edits_by_uri: Dict[str, List[Union[TextEdit, AnnotatedTextEdit]]] = ( 202 | defaultdict(list) 203 | ) 204 | for text_edit in text_document_edit.edits: 205 | cell_text_edit = self.cell_text_edit(text_edit) 206 | if cell_text_edit is not None: 207 | edits_by_uri[cell_text_edit.uri].append( 208 | cell_text_edit.text_edit 209 | ) 210 | 211 | # Yield per-cell text document edits. 212 | for uri, edits in edits_by_uri.items(): 213 | cell = self._cell_by_uri[uri] 214 | version = 0 if cell.version is None else cell.version 215 | yield TextDocumentEdit( 216 | text_document=OptionalVersionedTextDocumentIdentifier( 217 | uri=cell.uri, version=version 218 | ), 219 | edits=edits, 220 | ) 221 | 222 | 223 | def text_document_or_cell_locations( 224 | workspace: Workspace, locations: Optional[List[Location]] 225 | ) -> Optional[List[Location]]: 226 | """Convert concatenated notebook locations to cell locations, leaving text document locations as-is.""" 227 | if locations is None: 228 | return None 229 | 230 | results = [] 231 | for location in locations: 232 | mapper = notebook_coordinate_mapper( 233 | workspace, notebook_uri=location.uri 234 | ) 235 | if mapper is not None: 236 | cell_location = mapper.cell_location(location) 237 | if cell_location is not None: 238 | location = cell_location 239 | 240 | results.append(location) 241 | 242 | return results if results else None 243 | 244 | 245 | def cell_filename( 246 | workspace: Workspace, 247 | cell_uri: str, 248 | ) -> str: 249 | """Get the filename (used in diagnostics) for a cell URI.""" 250 | mapper = notebook_coordinate_mapper(workspace, cell_uri=cell_uri) 251 | if mapper is None: 252 | raise ValueError( 253 | f"Notebook document not found for cell URI: {cell_uri}" 254 | ) 255 | index = mapper.cell_index(cell_uri) 256 | assert index is not None 257 | return f"cell {index + 1}" 258 | 259 | 260 | T_ls = TypeVar("T_ls", bound=LanguageServer) 261 | 262 | T_params = TypeVar( 263 | "T_params", 264 | CallHierarchyPrepareParams, 265 | CodeActionParams, 266 | ColorPresentationParams, 267 | CompletionParams, 268 | DefinitionParams, 269 | DocumentHighlightParams, 270 | DocumentOnTypeFormattingParams, 271 | HoverParams, 272 | InlayHintParams, 273 | InlineValueParams, 274 | PrepareRenameParams, 275 | ReferenceParams, 276 | RenameParams, 277 | SemanticTokensRangeParams, 278 | SignatureHelpParams, 279 | TextDocumentPositionParams, 280 | ) 281 | 282 | T = TypeVar("T") 283 | 284 | 285 | class ServerWrapper(LanguageServer): 286 | def __init__(self, server: LanguageServer): 287 | self._wrapped = server 288 | self._workspace = WorkspaceWrapper(server.workspace) 289 | 290 | @property 291 | def workspace(self) -> Workspace: 292 | return self._workspace 293 | 294 | def __getattr__(self, name: str) -> Any: 295 | return getattr(self._wrapped, name) 296 | 297 | 298 | class WorkspaceWrapper(Workspace): 299 | def __init__(self, workspace: Workspace): 300 | self._wrapped = workspace 301 | 302 | def __getattr__(self, name: str) -> Any: 303 | return getattr(self._wrapped, name) 304 | 305 | def get_text_document(self, doc_uri: str) -> TextDocument: 306 | mapper = notebook_coordinate_mapper(self._wrapped, cell_uri=doc_uri) 307 | if mapper is None: 308 | return self._wrapped.get_text_document(doc_uri) 309 | return TextDocument( 310 | uri=mapper.notebook_uri, source=mapper.notebook_source 311 | ) 312 | 313 | 314 | def _notebook_params( 315 | mapper: NotebookCoordinateMapper, params: T_params 316 | ) -> T_params: 317 | if hasattr(params, "position"): 318 | notebook_position = mapper.notebook_position( 319 | params.text_document.uri, params.position 320 | ) 321 | # Ignore mypy error since it doesn't seem to narrow via hasattr. 322 | params = attrs.evolve(params, position=notebook_position) # type: ignore[call-arg] 323 | 324 | if hasattr(params, "range"): 325 | notebook_range = mapper.notebook_range( 326 | params.text_document.uri, params.range 327 | ) 328 | # Ignore mypy error since it doesn't seem to narrow via hasattr. 329 | params = attrs.evolve(params, range=notebook_range) # type: ignore[call-arg] 330 | 331 | return params 332 | 333 | 334 | def _cell_results( 335 | workspace: Workspace, 336 | mapper: Optional[NotebookCoordinateMapper], 337 | cell_uri: str, 338 | result: T, 339 | ) -> T: 340 | if isinstance(result, list) and result and isinstance(result[0], Location): 341 | return cast(T, text_document_or_cell_locations(workspace, result)) 342 | 343 | if ( 344 | mapper is not None 345 | and isinstance(result, Hover) 346 | and result.range is not None 347 | ): 348 | location = mapper.cell_range(result.range) 349 | if location is not None and location.uri == cell_uri: 350 | return cast(T, attrs.evolve(result, range=location.range)) 351 | 352 | return result 353 | 354 | 355 | def supports_notebooks( 356 | f: Callable[[T_ls, T_params], T], 357 | ) -> Callable[[T_ls, T_params], T]: 358 | """Decorator to add basic notebook support to a language server feature. 359 | 360 | It works by converting params from cell coordinates to notebook coordinates 361 | before calling the wrapped function, and then converting the result back 362 | to cell coordinates. 363 | """ 364 | 365 | def wrapped(ls: T_ls, params: T_params) -> T: 366 | mapper = notebook_coordinate_mapper( 367 | ls.workspace, cell_uri=params.text_document.uri 368 | ) 369 | notebook_params = ( 370 | _notebook_params(mapper, params) if mapper else params 371 | ) 372 | notebook_server = cast(T_ls, ServerWrapper(ls)) 373 | result = f(notebook_server, notebook_params) 374 | return _cell_results( 375 | notebook_server.workspace, mapper, params.text_document.uri, result 376 | ) 377 | 378 | return wrapped 379 | -------------------------------------------------------------------------------- /jedi_language_server/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pappasam/jedi-language-server/63744d56926f197df5995b309b832c7d0e084526/jedi_language_server/py.typed -------------------------------------------------------------------------------- /jedi_language_server/pygls_utils.py: -------------------------------------------------------------------------------- 1 | """Utilities to work with pygls. 2 | 3 | Helper functions that simplify working with pygls 4 | """ 5 | 6 | from typing import Optional 7 | 8 | from lsprotocol.types import Position, Range 9 | from pygls.workspace import TextDocument 10 | 11 | 12 | def char_before_cursor( 13 | document: TextDocument, position: Position, default: str = "" 14 | ) -> str: 15 | """Get the character directly before the cursor.""" 16 | try: 17 | return document.lines[position.line][position.character - 1] 18 | except IndexError: 19 | return default 20 | 21 | 22 | def char_after_cursor( 23 | document: TextDocument, position: Position, default: str = "" 24 | ) -> str: 25 | """Get the character directly before the cursor.""" 26 | try: 27 | return document.lines[position.line][position.character] 28 | except IndexError: 29 | return default 30 | 31 | 32 | def current_word_range( 33 | document: TextDocument, position: Position 34 | ) -> Optional[Range]: 35 | """Get the range of the word under the cursor.""" 36 | word = document.word_at_position(position) 37 | word_len = len(word) 38 | line: str = document.lines[position.line] 39 | start = 0 40 | for _ in range(1000): # prevent infinite hanging in case we hit edge case 41 | begin = line.find(word, start) 42 | if begin == -1: 43 | return None 44 | end = begin + word_len 45 | if begin <= position.character <= end: 46 | return Range( 47 | start=Position(line=position.line, character=begin), 48 | end=Position(line=position.line, character=end), 49 | ) 50 | start = end 51 | return None 52 | -------------------------------------------------------------------------------- /jedi_language_server/text_edit_utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions for converting to TextEdit. 2 | 3 | This module is a bridge between `jedi.Refactoring` and 4 | `pygls.types.TextEdit` types 5 | """ 6 | 7 | import ast 8 | import difflib 9 | from bisect import bisect_right 10 | from typing import Iterator, List, NamedTuple, Union 11 | 12 | from jedi.api.refactoring import ChangedFile, Refactoring 13 | from lsprotocol.types import ( 14 | AnnotatedTextEdit, 15 | CreateFile, 16 | DeleteFile, 17 | OptionalVersionedTextDocumentIdentifier, 18 | Position, 19 | Range, 20 | RenameFile, 21 | RenameFileOptions, 22 | TextDocumentEdit, 23 | TextEdit, 24 | ) 25 | from pygls.workspace import Workspace 26 | 27 | from . import notebook_utils 28 | 29 | 30 | def is_valid_python(code: str) -> bool: 31 | """Check whether Python code is syntactically valid.""" 32 | try: 33 | ast.parse(code) 34 | except SyntaxError: 35 | return False 36 | return True 37 | 38 | 39 | def lsp_document_changes( 40 | workspace: Workspace, 41 | refactoring: Refactoring, 42 | ) -> List[Union[TextDocumentEdit, RenameFile, CreateFile, DeleteFile]]: 43 | """Get lsp text document edits from Jedi refactoring. 44 | 45 | This is the main public function that you probably want 46 | """ 47 | converter = RefactoringConverter(workspace, refactoring) 48 | return [ 49 | *converter.lsp_text_document_edits(), 50 | *converter.lsp_renames(), 51 | ] 52 | 53 | 54 | class RefactoringConverter: 55 | """Convert jedi Refactoring objects into renaming machines.""" 56 | 57 | def __init__(self, workspace: Workspace, refactoring: Refactoring) -> None: 58 | self.workspace = workspace 59 | self.refactoring = refactoring 60 | 61 | def lsp_renames(self) -> Iterator[RenameFile]: 62 | """Get all File rename operations.""" 63 | for old_name, new_name in self.refactoring.get_renames(): 64 | yield RenameFile( 65 | kind="rename", 66 | old_uri=old_name.as_uri(), 67 | new_uri=new_name.as_uri(), 68 | options=RenameFileOptions( 69 | ignore_if_exists=True, overwrite=True 70 | ), 71 | ) 72 | 73 | def lsp_text_document_edits(self) -> Iterator[TextDocumentEdit]: 74 | """Get all text document edits.""" 75 | changed_files = self.refactoring.get_changed_files() 76 | for path, changed_file in changed_files.items(): 77 | uri = path.as_uri() 78 | document = self.workspace.get_text_document(uri) 79 | notebook_mapper = notebook_utils.notebook_coordinate_mapper( 80 | self.workspace, notebook_uri=uri 81 | ) 82 | source = ( 83 | notebook_mapper.notebook_source 84 | if notebook_mapper 85 | else document.source 86 | ) 87 | version = 0 if document.version is None else document.version 88 | text_edits = lsp_text_edits(source, changed_file) 89 | if text_edits: 90 | text_document_edit = TextDocumentEdit( 91 | text_document=OptionalVersionedTextDocumentIdentifier( 92 | uri=uri, 93 | version=version, 94 | ), 95 | edits=text_edits, 96 | ) 97 | if notebook_mapper is not None: 98 | yield from notebook_mapper.cell_text_document_edits( 99 | text_document_edit 100 | ) 101 | else: 102 | yield text_document_edit 103 | 104 | 105 | _OPCODES_CHANGE = {"replace", "delete", "insert"} 106 | 107 | 108 | def lsp_text_edits( 109 | old_code: str, changed_file: ChangedFile 110 | ) -> List[Union[TextEdit, AnnotatedTextEdit]]: 111 | """Take a jedi `ChangedFile` and convert to list of text edits. 112 | 113 | Handles inserts, replaces, and deletions within a text file. 114 | 115 | Additionally, makes sure returned code is syntactically valid 116 | Python. 117 | """ 118 | new_code = changed_file.get_new_code() 119 | if not is_valid_python(new_code): 120 | return [] 121 | 122 | position_lookup = PositionLookup(old_code) 123 | text_edits: List[Union[TextEdit, AnnotatedTextEdit]] = [] 124 | for opcode in get_opcodes(old_code, new_code): 125 | if opcode.op in _OPCODES_CHANGE: 126 | start = position_lookup.get(opcode.old_start) 127 | end = position_lookup.get(opcode.old_end) 128 | new_text = new_code[opcode.new_start : opcode.new_end] 129 | text_edits.append( 130 | TextEdit( 131 | range=Range(start=start, end=end), 132 | new_text=new_text, 133 | ) 134 | ) 135 | return text_edits 136 | 137 | 138 | class Opcode(NamedTuple): 139 | """Typed opcode. 140 | 141 | Op can be one of the following values: 142 | 'replace': a[i1:i2] should be replaced by b[j1:j2] 143 | 'delete': a[i1:i2] should be deleted. 144 | Note that j1==j2 in this case. 145 | 'insert': b[j1:j2] should be inserted at a[i1:i1]. 146 | Note that i1==i2 in this case. 147 | 'equal': a[i1:i2] == b[j1:j2] 148 | """ 149 | 150 | op: str 151 | old_start: int 152 | old_end: int 153 | new_start: int 154 | new_end: int 155 | 156 | 157 | def get_opcodes(old: str, new: str) -> List[Opcode]: 158 | """Obtain typed opcodes from two files (old and new).""" 159 | diff = difflib.SequenceMatcher(a=old, b=new) 160 | return [Opcode(*opcode) for opcode in diff.get_opcodes()] 161 | 162 | 163 | class PositionLookup: 164 | """Data structure to convert byte offset file to line number and character.""" 165 | 166 | def __init__(self, code: str) -> None: 167 | # Create a list saying at what offset in the file each line starts. 168 | self.line_starts = [] 169 | offset = 0 170 | for line in code.splitlines(keepends=True): 171 | self.line_starts.append(offset) 172 | offset += len(line) 173 | 174 | def get(self, offset: int) -> Position: 175 | """Get the position in the file that corresponds to the given offset.""" 176 | line = bisect_right(self.line_starts, offset) - 1 177 | character = offset - self.line_starts[line] 178 | return Position(line=line, character=character) 179 | -------------------------------------------------------------------------------- /jedi_language_server/type_map.py: -------------------------------------------------------------------------------- 1 | """Jedi types mapped to LSP types.""" 2 | 3 | from lsprotocol.types import CompletionItemKind, SymbolKind 4 | 5 | # See docs: 6 | # https://jedi.readthedocs.io/en/latest/docs/api-classes.html?highlight=type#jedi.api.classes.BaseName.type 7 | 8 | _JEDI_COMPLETION_TYPE_MAP = { 9 | "module": CompletionItemKind.Module, 10 | "class": CompletionItemKind.Class, 11 | "instance": CompletionItemKind.Variable, 12 | "function": CompletionItemKind.Function, 13 | "param": CompletionItemKind.Variable, 14 | "path": CompletionItemKind.File, 15 | "keyword": CompletionItemKind.Keyword, 16 | "property": CompletionItemKind.Property, 17 | "statement": CompletionItemKind.Variable, 18 | } 19 | 20 | _JEDI_SYMBOL_TYPE_MAP = { 21 | "module": SymbolKind.Module, 22 | "class": SymbolKind.Class, 23 | "instance": SymbolKind.Variable, 24 | "function": SymbolKind.Function, 25 | "param": SymbolKind.Variable, 26 | "path": SymbolKind.File, 27 | "keyword": SymbolKind.Class, 28 | "property": SymbolKind.Property, 29 | "statement": SymbolKind.Variable, 30 | } 31 | 32 | 33 | def get_lsp_completion_type(jedi_type: str) -> CompletionItemKind: 34 | """Get type map. 35 | 36 | Always return a value. 37 | """ 38 | return _JEDI_COMPLETION_TYPE_MAP.get(jedi_type, CompletionItemKind.Text) 39 | 40 | 41 | def get_lsp_symbol_type(jedi_type: str) -> SymbolKind: 42 | """Get type map. 43 | 44 | Always return a value. 45 | """ 46 | return _JEDI_SYMBOL_TYPE_MAP.get(jedi_type, SymbolKind.Namespace) 47 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """Configure nox.""" 2 | 3 | import nox 4 | 5 | NOX_SESSION = nox.session(python=False) 6 | 7 | 8 | @NOX_SESSION 9 | def fix(session: nox.Session): 10 | """Fix files inplace.""" 11 | session.run("ruff", "format", "-s", ".") 12 | session.run("ruff", "check", "-se", "--fix", ".") 13 | 14 | 15 | @NOX_SESSION 16 | def lint(session: nox.Session): 17 | """Check file formatting that only have to do with formatting.""" 18 | session.run("ruff", "format", "--check", ".") 19 | session.run("ruff", "check", ".") 20 | 21 | 22 | @NOX_SESSION 23 | def typecheck(session: nox.Session): 24 | session.run("mypy", "jedi_language_server") 25 | 26 | 27 | @NOX_SESSION 28 | def tests(session: nox.Session): 29 | session.run("slipcover", "-m", "pytest") 30 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.8.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.mypy] 6 | python_version = "3.9" 7 | strict = true 8 | enable_error_code = "ignore-without-code,redundant-expr,truthy-bool" 9 | 10 | [[tool.mypy.overrides]] 11 | module = "jedi.*" 12 | ignore_missing_imports = true 13 | 14 | [tool.poetry] 15 | name = "jedi-language-server" 16 | version = "0.45.1" 17 | description = "A language server for Jedi!" 18 | authors = ["Sam Roeca "] 19 | readme = "README.md" 20 | homepage = "https://github.com/pappasam/jedi-language-server" 21 | repository = "https://github.com/pappasam/jedi-language-server" 22 | keywords = [ 23 | "python", 24 | "completion", 25 | "refactoring", 26 | "lsp", 27 | "language-server-protocol", 28 | ] 29 | classifiers = [ 30 | "Development Status :: 4 - Beta", 31 | "Intended Audience :: Developers", 32 | "Operating System :: OS Independent", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | "Programming Language :: Python :: 3.13", 38 | "Topic :: Software Development :: Code Generators", 39 | "Topic :: Software Development :: Libraries :: Python Modules", 40 | "Topic :: Text Editors :: Integrated Development Environments (IDE)", 41 | "Topic :: Utilities", 42 | "Typing :: Typed", 43 | ] 44 | license = "MIT" 45 | 46 | [tool.poetry.dependencies] 47 | python = "^3.9" 48 | jedi = "^0.19.2" 49 | pygls = "^1.1.0" 50 | cattrs = ">=23.1.2" 51 | docstring-to-markdown = "0.*" 52 | lsprotocol = ">=2023.0.1" 53 | typing-extensions = { version = "^4.5.0", python = "<3.10" } 54 | 55 | [tool.poetry.group.dev.dependencies] 56 | PyHamcrest = "*" 57 | mypy = "*" 58 | nox = "*" 59 | pre-commit = "*" 60 | pytest = "*" 61 | python-lsp-jsonrpc = "*" 62 | ruff = "*" 63 | slipcover = { version = "*", python = "<3.14" } 64 | 65 | [tool.poetry.scripts] 66 | jedi-language-server = 'jedi_language_server.cli:cli' 67 | 68 | [tool.poetry.urls] 69 | Changelog = "https://github.com/pappasam/jedi-language-server/blob/main/CHANGELOG.md" 70 | Issues = "https://github.com/pappasam/jedi-language-server/issues" 71 | 72 | [tool.ruff] 73 | line-length = 79 74 | target-version = "py39" 75 | unsafe-fixes = true 76 | 77 | [tool.ruff.lint] 78 | select = [ 79 | "D", # pydocstyle 80 | "E", # pycodestyle 81 | "F", # pyflakes 82 | "I", # isort 83 | ] 84 | ignore = [ 85 | "D10", # missing-docstring 86 | "D206", # indent-with-spaces 87 | "D300", # triple-single-quotes 88 | "D401", # imperative-mood 89 | "E111", # indentation-with-invalid-multiple 90 | "E114", # indentation-with-invalid-multiple-comment 91 | "E117", # over-indented 92 | "E501", # line-too-long 93 | ] 94 | 95 | [tool.ruff.lint.per-file-ignores] 96 | "tests/test_data/*" = ["ALL"] 97 | 98 | [tool.ruff.lint.pydocstyle] 99 | convention = "pep257" 100 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Testing entrypoint.""" 2 | 3 | import py 4 | 5 | TEST_DATA = py.path.local(__file__) / ".." / "test_data" 6 | -------------------------------------------------------------------------------- /tests/lsp_test_client/__init__.py: -------------------------------------------------------------------------------- 1 | """Test client main module.""" 2 | 3 | import py 4 | 5 | from .utils import as_uri 6 | 7 | TEST_ROOT = py.path.local(__file__) / ".." 8 | PROJECT_ROOT = TEST_ROOT / ".." / ".." 9 | PROJECT_URI = as_uri(PROJECT_ROOT) 10 | -------------------------------------------------------------------------------- /tests/lsp_test_client/defaults.py: -------------------------------------------------------------------------------- 1 | """Default values for lsp test client.""" 2 | 3 | import os 4 | 5 | import tests.lsp_test_client as lsp_client 6 | 7 | VSCODE_DEFAULT_INITIALIZE = { 8 | "processId": os.getpid(), 9 | "clientInfo": {"name": "vscode", "version": "1.45.0"}, 10 | "rootPath": str(lsp_client.PROJECT_ROOT), 11 | "rootUri": lsp_client.PROJECT_URI, 12 | "capabilities": { 13 | "workspace": { 14 | "applyEdit": True, 15 | "workspaceEdit": { 16 | "documentChanges": True, 17 | "resourceOperations": ["create", "rename", "delete"], 18 | "failureHandling": "textOnlyTransactional", 19 | }, 20 | "didChangeConfiguration": {"dynamicRegistration": True}, 21 | "didChangeWatchedFiles": {"dynamicRegistration": True}, 22 | "symbol": { 23 | "dynamicRegistration": True, 24 | "symbolKind": { 25 | "valueSet": [ 26 | 1, 27 | 2, 28 | 3, 29 | 4, 30 | 5, 31 | 6, 32 | 7, 33 | 8, 34 | 9, 35 | 10, 36 | 11, 37 | 12, 38 | 13, 39 | 14, 40 | 15, 41 | 16, 42 | 17, 43 | 18, 44 | 19, 45 | 20, 46 | 21, 47 | 22, 48 | 23, 49 | 24, 50 | 25, 51 | 26, 52 | ] 53 | }, 54 | "tagSupport": {"valueSet": [1]}, 55 | }, 56 | "executeCommand": {"dynamicRegistration": True}, 57 | "configuration": True, 58 | "workspaceFolders": True, 59 | }, 60 | "textDocument": { 61 | "publishDiagnostics": { 62 | "relatedInformation": True, 63 | "versionSupport": False, 64 | "tagSupport": {"valueSet": [1, 2]}, 65 | "complexDiagnosticCodeSupport": True, 66 | }, 67 | "synchronization": { 68 | "dynamicRegistration": True, 69 | "willSave": True, 70 | "willSaveWaitUntil": True, 71 | "didSave": True, 72 | }, 73 | "completion": { 74 | "dynamicRegistration": True, 75 | "contextSupport": True, 76 | "completionItem": { 77 | "snippetSupport": True, 78 | "commitCharactersSupport": True, 79 | "documentationFormat": ["markdown", "plaintext"], 80 | "deprecatedSupport": True, 81 | "preselectSupport": True, 82 | "tagSupport": {"valueSet": [1]}, 83 | "insertReplaceSupport": True, 84 | }, 85 | "completionItemKind": { 86 | "valueSet": [ 87 | 1, 88 | 2, 89 | 3, 90 | 4, 91 | 5, 92 | 6, 93 | 7, 94 | 8, 95 | 9, 96 | 10, 97 | 11, 98 | 12, 99 | 13, 100 | 14, 101 | 15, 102 | 16, 103 | 17, 104 | 18, 105 | 19, 106 | 20, 107 | 21, 108 | 22, 109 | 23, 110 | 24, 111 | 25, 112 | ] 113 | }, 114 | }, 115 | "hover": { 116 | "dynamicRegistration": True, 117 | "contentFormat": ["markdown", "plaintext"], 118 | }, 119 | "signatureHelp": { 120 | "dynamicRegistration": True, 121 | "signatureInformation": { 122 | "documentationFormat": ["markdown", "plaintext"], 123 | "parameterInformation": {"labelOffsetSupport": True}, 124 | }, 125 | "contextSupport": True, 126 | }, 127 | "definition": {"dynamicRegistration": True, "linkSupport": True}, 128 | "references": {"dynamicRegistration": True}, 129 | "documentHighlight": {"dynamicRegistration": True}, 130 | "documentSymbol": { 131 | "dynamicRegistration": True, 132 | "symbolKind": { 133 | "valueSet": [ 134 | 1, 135 | 2, 136 | 3, 137 | 4, 138 | 5, 139 | 6, 140 | 7, 141 | 8, 142 | 9, 143 | 10, 144 | 11, 145 | 12, 146 | 13, 147 | 14, 148 | 15, 149 | 16, 150 | 17, 151 | 18, 152 | 19, 153 | 20, 154 | 21, 155 | 22, 156 | 23, 157 | 24, 158 | 25, 159 | 26, 160 | ] 161 | }, 162 | "hierarchicalDocumentSymbolSupport": True, 163 | "tagSupport": {"valueSet": [1]}, 164 | }, 165 | "codeAction": { 166 | "dynamicRegistration": True, 167 | "isPreferredSupport": True, 168 | "codeActionLiteralSupport": { 169 | "codeActionKind": { 170 | "valueSet": [ 171 | "", 172 | "quickfix", 173 | "refactor", 174 | "refactor.extract", 175 | "refactor.inline", 176 | "refactor.rewrite", 177 | "source", 178 | "source.organizeImports", 179 | ] 180 | } 181 | }, 182 | }, 183 | "codeLens": {"dynamicRegistration": True}, 184 | "formatting": {"dynamicRegistration": True}, 185 | "rangeFormatting": {"dynamicRegistration": True}, 186 | "onTypeFormatting": {"dynamicRegistration": True}, 187 | "rename": {"dynamicRegistration": True, "prepareSupport": True}, 188 | "documentLink": { 189 | "dynamicRegistration": True, 190 | "tooltipSupport": True, 191 | }, 192 | "typeDefinition": { 193 | "dynamicRegistration": True, 194 | "linkSupport": True, 195 | }, 196 | "implementation": { 197 | "dynamicRegistration": True, 198 | "linkSupport": True, 199 | }, 200 | "colorProvider": {"dynamicRegistration": True}, 201 | "foldingRange": { 202 | "dynamicRegistration": True, 203 | "rangeLimit": 5000, 204 | "lineFoldingOnly": True, 205 | }, 206 | "declaration": {"dynamicRegistration": True, "linkSupport": True}, 207 | "selectionRange": {"dynamicRegistration": True}, 208 | }, 209 | "window": {"workDoneProgress": True}, 210 | }, 211 | "trace": "verbose", 212 | "workspaceFolders": [{"uri": lsp_client.PROJECT_URI, "name": "jedi_lsp"}], 213 | "initializationOptions": { 214 | "diagnostics": { 215 | "enable": True, 216 | "didOpen": True, 217 | "didSave": True, 218 | "didChange": True, 219 | }, 220 | "workspace": {"symbols": {"maxSymbols": 0}}, 221 | }, 222 | } 223 | -------------------------------------------------------------------------------- /tests/lsp_test_client/lsp_run.py: -------------------------------------------------------------------------------- 1 | """Run Language Server for Test.""" 2 | 3 | import sys 4 | 5 | from jedi_language_server.cli import cli 6 | 7 | sys.exit(cli()) 8 | -------------------------------------------------------------------------------- /tests/lsp_test_client/session.py: -------------------------------------------------------------------------------- 1 | """Provides LSP session helpers for testing.""" 2 | 3 | import json 4 | import os 5 | import subprocess 6 | import sys 7 | from concurrent.futures import Future, ThreadPoolExecutor 8 | from threading import Event 9 | 10 | from pylsp_jsonrpc.dispatchers import MethodDispatcher 11 | from pylsp_jsonrpc.endpoint import Endpoint 12 | from pylsp_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter 13 | 14 | from tests.lsp_test_client import defaults 15 | from tests.lsp_test_client.utils import as_uri 16 | 17 | LSP_EXIT_TIMEOUT = 5000 18 | 19 | 20 | PUBLISH_DIAGNOSTICS = "textDocument/publishDiagnostics" 21 | WINDOW_LOG_MESSAGE = "window/logMessage" 22 | WINDOW_SHOW_MESSAGE = "window/showMessage" 23 | 24 | 25 | class LspSession(MethodDispatcher): 26 | """Send and Receive messages over LSP as a test LS Client.""" 27 | 28 | def __init__(self, cwd=None): 29 | self.cwd = cwd if cwd else os.getcwd() 30 | self._thread_pool = ThreadPoolExecutor() 31 | self._sub = None 32 | self._writer = None 33 | self._reader = None 34 | self._endpoint = None 35 | self._notification_callbacks = {} 36 | 37 | def __enter__(self): 38 | """Context manager entrypoint. 39 | 40 | shell=True needed for pytest-cov to work in subprocess. 41 | """ 42 | self._sub = subprocess.Popen( 43 | [ 44 | sys.executable, 45 | os.path.join(os.path.dirname(__file__), "lsp_run.py"), 46 | ], 47 | stdout=subprocess.PIPE, 48 | stdin=subprocess.PIPE, 49 | bufsize=0, 50 | cwd=self.cwd, 51 | env=os.environ, 52 | shell="WITH_COVERAGE" in os.environ, 53 | ) 54 | 55 | self._writer = JsonRpcStreamWriter( 56 | os.fdopen(self._sub.stdin.fileno(), "wb") 57 | ) 58 | self._reader = JsonRpcStreamReader( 59 | os.fdopen(self._sub.stdout.fileno(), "rb") 60 | ) 61 | 62 | dispatcher = { 63 | PUBLISH_DIAGNOSTICS: self._publish_diagnostics, 64 | WINDOW_SHOW_MESSAGE: self._window_show_message, 65 | WINDOW_LOG_MESSAGE: self._window_log_message, 66 | } 67 | self._endpoint = Endpoint(dispatcher, self._writer.write) 68 | self._thread_pool.submit(self._reader.listen, self._endpoint.consume) 69 | 70 | self._last_cell_id = 0 71 | return self 72 | 73 | def __exit__(self, typ, value, _tb): 74 | self.shutdown(True) 75 | try: 76 | self._sub.terminate() 77 | except Exception: 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 | server_initialized = Event() 89 | 90 | def _after_initialize(fut): 91 | if process_server_capabilities: 92 | process_server_capabilities(fut.result()) 93 | self.initialized() 94 | server_initialized.set() 95 | 96 | self._send_request( 97 | "initialize", 98 | params=( 99 | initialize_params 100 | if initialize_params is not None 101 | else defaults.VSCODE_DEFAULT_INITIALIZE 102 | ), 103 | handle_response=_after_initialize, 104 | ) 105 | 106 | server_initialized.wait() 107 | 108 | def initialized(self, initialized_params=None): 109 | """Sends the initialized notification to LSP server.""" 110 | if initialized_params is None: 111 | initialized_params = {} 112 | self._endpoint.notify("initialized", initialized_params) 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 text_document_completion(self, completion_params): 129 | """Sends text document completion request to LSP server.""" 130 | fut = self._send_request( 131 | "textDocument/completion", params=completion_params 132 | ) 133 | return fut.result() 134 | 135 | def text_document_rename(self, rename_params): 136 | """Sends text document rename request to LSP server.""" 137 | fut = self._send_request("textDocument/rename", params=rename_params) 138 | return fut.result() 139 | 140 | def text_document_code_action(self, code_action_params): 141 | """Sends text document code action request to LSP server.""" 142 | fut = self._send_request( 143 | "textDocument/codeAction", params=code_action_params 144 | ) 145 | return fut.result() 146 | 147 | def text_document_hover(self, hover_params): 148 | """Sends text document hover request to LSP server.""" 149 | fut = self._send_request("textDocument/hover", params=hover_params) 150 | return fut.result() 151 | 152 | def text_document_signature_help(self, signature_help_params): 153 | """Sends text document hover request to LSP server.""" 154 | fut = self._send_request( 155 | "textDocument/signatureHelp", params=signature_help_params 156 | ) 157 | return fut.result() 158 | 159 | def text_document_declaration(self, declaration_params): 160 | """Sends text document declaration request to LSP server.""" 161 | fut = self._send_request( 162 | "textDocument/declaration", params=declaration_params 163 | ) 164 | return fut.result() 165 | 166 | def text_document_definition(self, definition_params): 167 | """Sends text document definition request to LSP server.""" 168 | fut = self._send_request( 169 | "textDocument/definition", params=definition_params 170 | ) 171 | return fut.result() 172 | 173 | def text_document_symbol(self, document_symbol_params): 174 | """Sends text document symbol request to LSP server.""" 175 | fut = self._send_request( 176 | "textDocument/documentSymbol", params=document_symbol_params 177 | ) 178 | return fut.result() 179 | 180 | def text_document_highlight(self, document_highlight_params): 181 | """Sends text document highlight request to LSP server.""" 182 | fut = self._send_request( 183 | "textDocument/documentHighlight", params=document_highlight_params 184 | ) 185 | return fut.result() 186 | 187 | def text_document_references(self, references_params): 188 | """Sends text document references request to LSP server.""" 189 | fut = self._send_request( 190 | "textDocument/references", params=references_params 191 | ) 192 | return fut.result() 193 | 194 | def text_doc_semantic_tokens_full(self, semantic_tokens_params): 195 | """Sends text document semantic tokens full request to LSP server.""" 196 | fut = self._send_request( 197 | "textDocument/semanticTokens/full", params=semantic_tokens_params 198 | ) 199 | return fut.result() 200 | 201 | def text_doc_semantic_tokens_range(self, semantic_tokens_range_params): 202 | """Sends text document semantic tokens range request to LSP server.""" 203 | fut = self._send_request( 204 | "textDocument/semanticTokens/range", 205 | params=semantic_tokens_range_params, 206 | ) 207 | return fut.result() 208 | 209 | def workspace_symbol(self, workspace_symbol_params): 210 | """Sends workspace symbol request to LSP server.""" 211 | fut = self._send_request( 212 | "workspace/symbol", params=workspace_symbol_params 213 | ) 214 | return fut.result() 215 | 216 | def completion_item_resolve(self, resolve_params): 217 | """Sends completion item resolve request to LSP server.""" 218 | fut = self._send_request( 219 | "completionItem/resolve", params=resolve_params 220 | ) 221 | return fut.result() 222 | 223 | def notify_did_change_text_document(self, did_change_params): 224 | """Sends did change text document notification to LSP Server.""" 225 | self._send_notification( 226 | "textDocument/didChange", params=did_change_params 227 | ) 228 | 229 | def notify_did_save_text_document(self, did_save_params): 230 | """Sends did save text document notification to LSP Server.""" 231 | self._send_notification("textDocument/didSave", params=did_save_params) 232 | 233 | def notify_did_open_text_document(self, did_open_params): 234 | """Sends did open text document notification to LSP Server.""" 235 | self._send_notification("textDocument/didOpen", params=did_open_params) 236 | 237 | def notify_did_close_text_document(self, did_close_params): 238 | """Sends did close text document notification to LSP Server.""" 239 | self._send_notification( 240 | "textDocument/didClose", params=did_close_params 241 | ) 242 | 243 | def notify_did_change_notebook_document(self, did_change_params): 244 | """Sends did change notebook document notification to LSP Server.""" 245 | self._send_notification( 246 | "notebookDocument/didChange", params=did_change_params 247 | ) 248 | 249 | def notify_did_save_notebook_document(self, did_save_params): 250 | """Sends did save notebook document notification to LSP Server.""" 251 | self._send_notification( 252 | "notebookDocument/didSave", params=did_save_params 253 | ) 254 | 255 | def notify_did_open_notebook_document(self, did_open_params): 256 | """Sends did open notebook document notification to LSP Server.""" 257 | self._send_notification( 258 | "notebookDocument/didOpen", params=did_open_params 259 | ) 260 | 261 | def notify_did_close_notebook_document(self, did_close_params): 262 | """Sends did close notebook document notification to LSP Server.""" 263 | self._send_notification( 264 | "notebookDocument/didClose", params=did_close_params 265 | ) 266 | 267 | def open_notebook_document(self, path): 268 | """Opens a notebook document on the LSP Server.""" 269 | # Construct did_open_notebook_document params from the notebook file. 270 | notebook = json.loads(path.read_text("utf-8")) 271 | uri = as_uri(path) 272 | lsp_cells = [] 273 | lsp_cell_text_documents = [] 274 | for cell in notebook["cells"]: 275 | self._last_cell_id += 1 276 | cell_uri = f"{uri}#{self._last_cell_id}" 277 | lsp_cells.append( 278 | { 279 | "kind": 2 if cell["cell_type"] == "code" else 1, 280 | "document": cell_uri, 281 | "metadata": {"metadata": cell["metadata"]}, 282 | } 283 | ) 284 | lsp_cell_text_documents.append( 285 | { 286 | "uri": cell_uri, 287 | "languageId": "python", 288 | "version": 1, 289 | "text": "".join(cell["source"]), 290 | } 291 | ) 292 | 293 | # Notify the server. 294 | self.notify_did_open_notebook_document( 295 | { 296 | "notebookDocument": { 297 | "uri": uri, 298 | "notebookType": "jupyter-notebook", 299 | "languageId": "python", 300 | "version": 1, 301 | "cells": lsp_cells, 302 | }, 303 | "cellTextDocuments": lsp_cell_text_documents, 304 | } 305 | ) 306 | 307 | # Return the generated cell URIs. 308 | return [cell["document"] for cell in lsp_cells] 309 | 310 | def set_notification_callback(self, notification_name, callback): 311 | """Set custom LS notification handler.""" 312 | self._notification_callbacks[notification_name] = callback 313 | 314 | def get_notification_callback(self, notification_name): 315 | """Gets callback if set or default callback for a given LS notification.""" 316 | try: 317 | return self._notification_callbacks[notification_name] 318 | except KeyError: 319 | 320 | def _default_handler(_params): 321 | """Default notification handler.""" 322 | 323 | return _default_handler 324 | 325 | def _publish_diagnostics(self, publish_diagnostics_params): 326 | """Internal handler for text document publish diagnostics.""" 327 | return self._handle_notification( 328 | PUBLISH_DIAGNOSTICS, publish_diagnostics_params 329 | ) 330 | 331 | def _window_log_message(self, window_log_message_params): 332 | """Internal handler for window log message.""" 333 | return self._handle_notification( 334 | WINDOW_LOG_MESSAGE, window_log_message_params 335 | ) 336 | 337 | def _window_show_message(self, window_show_message_params): 338 | """Internal handler for window show message.""" 339 | return self._handle_notification( 340 | WINDOW_SHOW_MESSAGE, window_show_message_params 341 | ) 342 | 343 | def _handle_notification(self, notification_name, params): 344 | """Internal handler for notifications.""" 345 | fut = Future() 346 | 347 | def _handler(): 348 | callback = self.get_notification_callback(notification_name) 349 | callback(params) 350 | fut.set_result(None) 351 | 352 | self._thread_pool.submit(_handler) 353 | return fut 354 | 355 | def _send_request( 356 | self, name, params=None, handle_response=lambda f: f.done() 357 | ): 358 | """Sends {name} request to the LSP server.""" 359 | fut = self._endpoint.request(name, params) 360 | fut.add_done_callback(handle_response) 361 | return fut 362 | 363 | def _send_notification(self, name, params=None): 364 | """Sends {name} notification to the LSP server.""" 365 | self._endpoint.notify(name, params) 366 | -------------------------------------------------------------------------------- /tests/lsp_test_client/utils.py: -------------------------------------------------------------------------------- 1 | """Provides LSP client side utilities for easier testing.""" 2 | 3 | import os 4 | import pathlib 5 | import platform 6 | import re 7 | from random import choice 8 | 9 | import py 10 | 11 | 12 | def normalizecase(path: str) -> str: 13 | """Fixes 'file' uri or path case for easier testing in windows.""" 14 | if platform.system() == "Windows": 15 | return path.lower() 16 | return path 17 | 18 | 19 | def as_uri(path: py.path.local) -> str: 20 | """Return 'file' uri as string.""" 21 | return normalizecase(pathlib.Path(path).as_uri()) 22 | 23 | 24 | class StringPattern: 25 | """Matches string patterns.""" 26 | 27 | def __init__(self, pattern): 28 | self.pattern = pattern 29 | 30 | def __eq__(self, compare): 31 | """Compares against pattern when possible.""" 32 | if isinstance(compare, str): 33 | match = re.match(self.pattern, compare) 34 | return match is not None 35 | 36 | if isinstance(compare, StringPattern): 37 | return self.pattern == compare.pattern 38 | 39 | return False 40 | 41 | def match(self, test_str): 42 | """Returns matches if pattern matches are found in the test string.""" 43 | return re.match(self.pattern, test_str) 44 | 45 | 46 | class PythonFile: 47 | """Create python file on demand for testing.""" 48 | 49 | def __init__(self, contents, root): 50 | self.contents = contents 51 | self.basename = "".join( 52 | choice("abcdefghijklmnopqrstuvwxyz") if i < 8 else ".py" 53 | for i in range(9) 54 | ) 55 | self.fullpath = py.path.local(root) / self.basename 56 | 57 | def __enter__(self): 58 | """Creates a python file for testing.""" 59 | with open(self.fullpath, "w", encoding="utf8") as py_file: 60 | py_file.write(self.contents) 61 | return self 62 | 63 | def __exit__(self, typ, value, _tb): 64 | """Cleans up and deletes the python file.""" 65 | os.unlink(self.fullpath) 66 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_completion.py: -------------------------------------------------------------------------------- 1 | """Tests for document completion requests.""" 2 | 3 | import copy 4 | 5 | from hamcrest import assert_that, is_ 6 | 7 | from tests import TEST_DATA 8 | from tests.lsp_test_client import session 9 | from tests.lsp_test_client.defaults import VSCODE_DEFAULT_INITIALIZE 10 | from tests.lsp_test_client.utils import as_uri 11 | 12 | COMPLETION_TEST_ROOT = TEST_DATA / "completion" 13 | 14 | 15 | def test_lsp_completion() -> None: 16 | """Test a simple completion request. 17 | 18 | Test Data: tests/test_data/completion/completion_test1.py 19 | """ 20 | with session.LspSession() as ls_session: 21 | ls_session.initialize() 22 | uri = as_uri(COMPLETION_TEST_ROOT / "completion_test1.py") 23 | actual = ls_session.text_document_completion( 24 | { 25 | "textDocument": {"uri": uri}, 26 | "position": {"line": 8, "character": 2}, 27 | "context": {"triggerKind": 1}, 28 | } 29 | ) 30 | 31 | expected = { 32 | "isIncomplete": False, 33 | "items": [ 34 | { 35 | "label": "my_function", 36 | "kind": 3, 37 | "sortText": "v0", 38 | "filterText": "my_function", 39 | "insertText": "my_function()$0", 40 | "insertTextFormat": 2, 41 | } 42 | ], 43 | } 44 | assert_that(actual, is_(expected)) 45 | 46 | actual = ls_session.completion_item_resolve( 47 | { 48 | "label": "my_function", 49 | "kind": 3, 50 | "sortText": "v0", 51 | "filterText": "my_function", 52 | "insertText": "my_function()$0", 53 | "insertTextFormat": 2, 54 | } 55 | ) 56 | expected = { 57 | "label": "my_function", 58 | "kind": 3, 59 | "detail": "def my_function()", 60 | "documentation": { 61 | "kind": "markdown", 62 | "value": "Simple test function.", 63 | }, 64 | "sortText": "v0", 65 | "filterText": "my_function", 66 | "insertText": "my_function()$0", 67 | "insertTextFormat": 2, 68 | } 69 | assert_that(actual, is_(expected)) 70 | 71 | 72 | def test_eager_lsp_completion() -> None: 73 | """Test a simple completion request, with eager resolution. 74 | 75 | Test Data: tests/test_data/completion/completion_test1.py 76 | """ 77 | with session.LspSession() as ls_session: 78 | # Initialize, asking for eager resolution. 79 | initialize_params = copy.deepcopy(VSCODE_DEFAULT_INITIALIZE) 80 | initialize_params["initializationOptions"] = { 81 | "completion": {"resolveEagerly": True} 82 | } 83 | ls_session.initialize(initialize_params) 84 | 85 | uri = as_uri(COMPLETION_TEST_ROOT / "completion_test1.py") 86 | actual = ls_session.text_document_completion( 87 | { 88 | "textDocument": {"uri": uri}, 89 | "position": {"line": 8, "character": 2}, 90 | "context": {"triggerKind": 1}, 91 | } 92 | ) 93 | 94 | expected = { 95 | "isIncomplete": False, 96 | "items": [ 97 | { 98 | "label": "my_function", 99 | "kind": 3, 100 | "detail": "def my_function()", 101 | "documentation": { 102 | "kind": "markdown", 103 | "value": "Simple test function.", 104 | }, 105 | "sortText": "v0", 106 | "filterText": "my_function", 107 | "insertText": "my_function()$0", 108 | "insertTextFormat": 2, 109 | } 110 | ], 111 | } 112 | assert_that(actual, is_(expected)) 113 | 114 | 115 | def test_lsp_completion_class_method() -> None: 116 | """Checks whether completion returns self unnecessarily. 117 | 118 | References: https://github.com/pappasam/jedi-language-server/issues/121 119 | 120 | Note: I resolve eagerly to make test simpler 121 | """ 122 | with session.LspSession() as ls_session: 123 | # Initialize, asking for eager resolution. 124 | initialize_params = copy.deepcopy(VSCODE_DEFAULT_INITIALIZE) 125 | initialize_params["initializationOptions"] = { 126 | "completion": {"resolveEagerly": True} 127 | } 128 | ls_session.initialize(initialize_params) 129 | 130 | uri = as_uri(COMPLETION_TEST_ROOT / "completion_test_class_self.py") 131 | actual = ls_session.text_document_completion( 132 | { 133 | "textDocument": {"uri": uri}, 134 | "position": {"line": 7, "character": 13}, 135 | "context": {"triggerKind": 1}, 136 | } 137 | ) 138 | 139 | expected = { 140 | "isIncomplete": False, 141 | "items": [ 142 | { 143 | "label": "some_method", 144 | "kind": 3, 145 | "detail": "def some_method(x)", 146 | "documentation": { 147 | "kind": "markdown", 148 | "value": "Great method.", 149 | }, 150 | "sortText": "v0", 151 | "filterText": "some_method", 152 | "insertText": "some_method(${1:x})$0", 153 | "insertTextFormat": 2, 154 | } 155 | ], 156 | } 157 | assert_that(actual, is_(expected)) 158 | 159 | 160 | def test_lsp_completion_class_noargs() -> None: 161 | """Checks if classes without arguments include parenthesis in signature.""" 162 | with session.LspSession() as ls_session: 163 | # Initialize, asking for eager resolution. 164 | initialize_params = copy.deepcopy(VSCODE_DEFAULT_INITIALIZE) 165 | initialize_params["initializationOptions"] = { 166 | "completion": {"resolveEagerly": True} 167 | } 168 | ls_session.initialize(initialize_params) 169 | 170 | uri = as_uri(COMPLETION_TEST_ROOT / "completion_test2.py") 171 | actual = ls_session.text_document_completion( 172 | { 173 | "textDocument": {"uri": uri}, 174 | "position": {"line": 7, "character": 3}, 175 | "context": {"triggerKind": 1}, 176 | } 177 | ) 178 | 179 | expected = { 180 | "isIncomplete": False, 181 | "items": [ 182 | { 183 | "label": "MyClass", 184 | "kind": 7, 185 | "detail": "class MyClass()", 186 | "documentation": { 187 | "kind": "markdown", 188 | "value": "Simple class.", 189 | }, 190 | "sortText": "v0", 191 | "filterText": "MyClass", 192 | "insertText": "MyClass()$0", 193 | "insertTextFormat": 2, 194 | } 195 | ], 196 | } 197 | assert_that(actual, is_(expected)) 198 | 199 | 200 | def test_lsp_completion_notebook() -> None: 201 | """Test a simple completion request, in a notebook. 202 | 203 | Test Data: tests/test_data/completion/completion_test1.ipynb 204 | """ 205 | with session.LspSession() as ls_session: 206 | ls_session.initialize() 207 | 208 | path = COMPLETION_TEST_ROOT / "completion_test1.ipynb" 209 | cell_uris = ls_session.open_notebook_document(path) 210 | actual = ls_session.text_document_completion( 211 | { 212 | "textDocument": {"uri": cell_uris[1]}, 213 | "position": {"line": 0, "character": 2}, 214 | "context": {"triggerKind": 1}, 215 | } 216 | ) 217 | 218 | expected = { 219 | "isIncomplete": False, 220 | "items": [ 221 | { 222 | "label": "my_function", 223 | "kind": 3, 224 | "sortText": "v0", 225 | "filterText": "my_function", 226 | "insertText": "my_function()$0", 227 | "insertTextFormat": 2, 228 | } 229 | ], 230 | } 231 | assert_that(actual, is_(expected)) 232 | 233 | actual = ls_session.completion_item_resolve( 234 | { 235 | "label": "my_function", 236 | "kind": 3, 237 | "sortText": "v0", 238 | "filterText": "my_function", 239 | "insertText": "my_function()$0", 240 | "insertTextFormat": 2, 241 | } 242 | ) 243 | expected = { 244 | "label": "my_function", 245 | "kind": 3, 246 | "detail": "def my_function()", 247 | "documentation": { 248 | "kind": "markdown", 249 | "value": "Simple test function.", 250 | }, 251 | "sortText": "v0", 252 | "filterText": "my_function", 253 | "insertText": "my_function()$0", 254 | "insertTextFormat": 2, 255 | } 256 | assert_that(actual, is_(expected)) 257 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_definition.py: -------------------------------------------------------------------------------- 1 | """Tests for definition requests.""" 2 | 3 | from hamcrest import assert_that, is_ 4 | 5 | from tests import TEST_DATA 6 | from tests.lsp_test_client import session 7 | from tests.lsp_test_client.utils import as_uri 8 | 9 | DEFINITION_TEST_ROOT = TEST_DATA / "definition" 10 | 11 | 12 | def test_definition(): 13 | """Tests definition on a function imported from module. 14 | 15 | Test Data: tests/test_data/definition/definition_test1.py 16 | """ 17 | with session.LspSession() as ls_session: 18 | ls_session.initialize() 19 | uri = as_uri(DEFINITION_TEST_ROOT / "definition_test1.py") 20 | actual = ls_session.text_document_definition( 21 | { 22 | "textDocument": {"uri": uri}, 23 | "position": {"line": 5, "character": 20}, 24 | } 25 | ) 26 | 27 | module_uri = as_uri(DEFINITION_TEST_ROOT / "somemodule2.py") 28 | expected = [ 29 | { 30 | "uri": module_uri, 31 | "range": { 32 | "start": {"line": 3, "character": 4}, 33 | "end": {"line": 3, "character": 17}, 34 | }, 35 | } 36 | ] 37 | 38 | assert_that(actual, is_(expected)) 39 | 40 | 41 | def test_definition_notebook(): 42 | """Tests definition on a function imported from a module, in a notebook. 43 | 44 | Test Data: tests/test_data/definition/definition_test1.ipynb 45 | """ 46 | with session.LspSession() as ls_session: 47 | ls_session.initialize() 48 | path = DEFINITION_TEST_ROOT / "definition_test1.ipynb" 49 | cell_uris = ls_session.open_notebook_document(path) 50 | actual = ls_session.text_document_definition( 51 | { 52 | "textDocument": {"uri": cell_uris[1]}, 53 | "position": {"line": 1, "character": 20}, 54 | } 55 | ) 56 | 57 | module_uri = as_uri(DEFINITION_TEST_ROOT / "somemodule2.py") 58 | expected = [ 59 | { 60 | "uri": module_uri, 61 | "range": { 62 | "start": {"line": 3, "character": 4}, 63 | "end": {"line": 3, "character": 17}, 64 | }, 65 | } 66 | ] 67 | 68 | assert_that(actual, is_(expected)) 69 | 70 | 71 | def test_declaration(): 72 | """Tests declaration on an imported module. 73 | 74 | Test Data: tests/test_data/definition/definition_test1.py 75 | """ 76 | with session.LspSession() as ls_session: 77 | ls_session.initialize() 78 | uri = as_uri(DEFINITION_TEST_ROOT / "definition_test1.py") 79 | actual = ls_session.text_document_declaration( 80 | { 81 | "textDocument": {"uri": uri}, 82 | "position": {"line": 5, "character": 0}, 83 | } 84 | ) 85 | 86 | expected = [ 87 | { 88 | "uri": uri, 89 | "range": { 90 | "start": {"line": 2, "character": 26}, 91 | "end": {"line": 2, "character": 37}, 92 | }, 93 | } 94 | ] 95 | 96 | assert_that(actual, is_(expected)) 97 | 98 | 99 | def test_declaration_notebook(): 100 | """Tests declaration on an imported module, in a notebook. 101 | 102 | Test Data: tests/test_data/definition/definition_test1.ipynb 103 | """ 104 | with session.LspSession() as ls_session: 105 | ls_session.initialize() 106 | path = DEFINITION_TEST_ROOT / "definition_test1.ipynb" 107 | cell_uris = ls_session.open_notebook_document(path) 108 | actual = ls_session.text_document_declaration( 109 | { 110 | "textDocument": {"uri": cell_uris[1]}, 111 | "position": {"line": 0, "character": 0}, 112 | } 113 | ) 114 | 115 | expected = [ 116 | { 117 | "uri": cell_uris[0], 118 | "range": { 119 | "start": {"line": 0, "character": 7}, 120 | "end": {"line": 0, "character": 17}, 121 | }, 122 | } 123 | ] 124 | 125 | assert_that(actual, is_(expected)) 126 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_highlighting.py: -------------------------------------------------------------------------------- 1 | """Tests for highlighting requests.""" 2 | 3 | import pytest 4 | from hamcrest import assert_that, is_ 5 | 6 | from tests import TEST_DATA 7 | from tests.lsp_test_client import session 8 | from tests.lsp_test_client.utils import as_uri 9 | 10 | HIGHLIGHTING_TEST_ROOT = TEST_DATA / "highlighting" 11 | 12 | 13 | @pytest.mark.parametrize( 14 | ["position", "expected"], 15 | [ 16 | ({"line": 2, "character": 3}, None), 17 | ( 18 | {"line": 2, "character": 8}, 19 | [ 20 | { 21 | "range": { 22 | "start": {"line": 2, "character": 5}, 23 | "end": {"line": 2, "character": 9}, 24 | } 25 | } 26 | ], 27 | ), 28 | ( 29 | {"line": 2, "character": 20}, 30 | [ 31 | { 32 | "range": { 33 | "start": {"line": 2, "character": 17}, 34 | "end": {"line": 2, "character": 21}, 35 | } 36 | }, 37 | { 38 | "range": { 39 | "start": {"line": 12, "character": 16}, 40 | "end": {"line": 12, "character": 20}, 41 | } 42 | }, 43 | ], 44 | ), 45 | ( 46 | {"line": 4, "character": 8}, 47 | [ 48 | { 49 | "range": { 50 | "start": {"line": 4, "character": 0}, 51 | "end": {"line": 4, "character": 13}, 52 | } 53 | }, 54 | { 55 | "range": { 56 | "start": {"line": 20, "character": 15}, 57 | "end": {"line": 20, "character": 28}, 58 | } 59 | }, 60 | { 61 | "range": { 62 | "start": {"line": 24, "character": 29}, 63 | "end": {"line": 24, "character": 42}, 64 | } 65 | }, 66 | ], 67 | ), 68 | ( 69 | {"line": 7, "character": 9}, 70 | [ 71 | { 72 | "range": { 73 | "start": {"line": 7, "character": 4}, 74 | "end": {"line": 7, "character": 17}, 75 | } 76 | }, 77 | { 78 | "range": { 79 | "start": {"line": 24, "character": 15}, 80 | "end": {"line": 24, "character": 28}, 81 | } 82 | }, 83 | ], 84 | ), 85 | ( 86 | {"line": 7, "character": 20}, 87 | [ 88 | { 89 | "range": { 90 | "start": {"line": 7, "character": 18}, 91 | "end": {"line": 7, "character": 21}, 92 | } 93 | }, 94 | { 95 | "range": { 96 | "start": {"line": 9, "character": 11}, 97 | "end": {"line": 9, "character": 14}, 98 | } 99 | }, 100 | ], 101 | ), 102 | ( 103 | {"line": 12, "character": 14}, 104 | [ 105 | { 106 | "range": { 107 | "start": {"line": 12, "character": 6}, 108 | "end": {"line": 12, "character": 15}, 109 | } 110 | }, 111 | { 112 | "range": { 113 | "start": {"line": 27, "character": 11}, 114 | "end": {"line": 27, "character": 20}, 115 | } 116 | }, 117 | ], 118 | ), 119 | ( 120 | {"line": 15, "character": 20}, 121 | [ 122 | { 123 | "range": { 124 | "start": {"line": 15, "character": 17}, 125 | "end": {"line": 15, "character": 21}, 126 | } 127 | }, 128 | { 129 | "range": { 130 | "start": {"line": 16, "character": 8}, 131 | "end": {"line": 16, "character": 12}, 132 | } 133 | }, 134 | ], 135 | ), 136 | ( 137 | {"line": 16, "character": 17}, 138 | [ 139 | { 140 | "range": { 141 | "start": {"line": 16, "character": 13}, 142 | "end": {"line": 16, "character": 19}, 143 | } 144 | }, 145 | { 146 | "range": { 147 | "start": {"line": 20, "character": 36}, 148 | "end": {"line": 20, "character": 42}, 149 | } 150 | }, 151 | ], 152 | ), 153 | ( 154 | {"line": 18, "character": 15}, 155 | [ 156 | { 157 | "range": { 158 | "start": {"line": 18, "character": 8}, 159 | "end": {"line": 18, "character": 20}, 160 | } 161 | }, 162 | { 163 | "range": { 164 | "start": {"line": 28, "character": 9}, 165 | "end": {"line": 28, "character": 21}, 166 | } 167 | }, 168 | ], 169 | ), 170 | # __file__ 171 | ( 172 | {"line": 34, "character": 8}, 173 | [ 174 | { 175 | "range": { 176 | "start": {"line": 34, "character": 6}, 177 | "end": {"line": 34, "character": 14}, 178 | } 179 | }, 180 | ], 181 | ), 182 | # __package__ 183 | ( 184 | {"line": 35, "character": 8}, 185 | [ 186 | { 187 | "range": { 188 | "start": {"line": 35, "character": 6}, 189 | "end": {"line": 35, "character": 17}, 190 | } 191 | }, 192 | ], 193 | ), 194 | # __doc__ 195 | ( 196 | {"line": 36, "character": 8}, 197 | [ 198 | { 199 | "range": { 200 | "start": {"line": 36, "character": 6}, 201 | "end": {"line": 36, "character": 13}, 202 | } 203 | }, 204 | ], 205 | ), 206 | # __name__ 207 | ( 208 | {"line": 37, "character": 8}, 209 | [ 210 | { 211 | "range": { 212 | "start": {"line": 37, "character": 6}, 213 | "end": {"line": 37, "character": 14}, 214 | } 215 | }, 216 | ], 217 | ), 218 | ], 219 | ) 220 | def test_highlighting(position, expected): 221 | """Tests highlighting on import statement. 222 | 223 | Test Data: tests/test_data/highlighting/highlighting_test1.py 224 | """ 225 | with session.LspSession() as ls_session: 226 | ls_session.initialize() 227 | uri = as_uri(HIGHLIGHTING_TEST_ROOT / "highlighting_test1.py") 228 | actual = ls_session.text_document_highlight( 229 | { 230 | "textDocument": {"uri": uri}, 231 | "position": position, 232 | } 233 | ) 234 | 235 | assert_that(actual, is_(expected)) 236 | 237 | 238 | @pytest.mark.parametrize( 239 | ["cell", "position", "expected"], 240 | [ 241 | (0, {"line": 2, "character": 3}, None), 242 | ( 243 | 0, 244 | {"line": 2, "character": 8}, 245 | [ 246 | { 247 | "range": { 248 | "start": {"line": 2, "character": 5}, 249 | "end": {"line": 2, "character": 9}, 250 | } 251 | } 252 | ], 253 | ), 254 | ( 255 | 0, 256 | {"line": 2, "character": 20}, 257 | [ 258 | { 259 | "range": { 260 | "start": {"line": 2, "character": 17}, 261 | "end": {"line": 2, "character": 21}, 262 | } 263 | }, 264 | { 265 | "range": { 266 | "start": {"line": 12, "character": 16}, 267 | "end": {"line": 12, "character": 20}, 268 | } 269 | }, 270 | ], 271 | ), 272 | ( 273 | 0, 274 | {"line": 4, "character": 8}, 275 | [ 276 | { 277 | "range": { 278 | "start": {"line": 4, "character": 0}, 279 | "end": {"line": 4, "character": 13}, 280 | } 281 | }, 282 | { 283 | "range": { 284 | "start": {"line": 20, "character": 15}, 285 | "end": {"line": 20, "character": 28}, 286 | } 287 | }, 288 | { 289 | "range": { 290 | "start": {"line": 24, "character": 29}, 291 | "end": {"line": 24, "character": 42}, 292 | } 293 | }, 294 | ], 295 | ), 296 | ( 297 | 0, 298 | {"line": 7, "character": 9}, 299 | [ 300 | { 301 | "range": { 302 | "start": {"line": 7, "character": 4}, 303 | "end": {"line": 7, "character": 17}, 304 | } 305 | }, 306 | { 307 | "range": { 308 | "start": {"line": 24, "character": 15}, 309 | "end": {"line": 24, "character": 28}, 310 | } 311 | }, 312 | ], 313 | ), 314 | ( 315 | 0, 316 | {"line": 7, "character": 20}, 317 | [ 318 | { 319 | "range": { 320 | "start": {"line": 7, "character": 18}, 321 | "end": {"line": 7, "character": 21}, 322 | } 323 | }, 324 | { 325 | "range": { 326 | "start": {"line": 9, "character": 11}, 327 | "end": {"line": 9, "character": 14}, 328 | } 329 | }, 330 | ], 331 | ), 332 | ( 333 | 0, 334 | {"line": 12, "character": 14}, 335 | [ 336 | { 337 | "range": { 338 | "start": {"line": 12, "character": 6}, 339 | "end": {"line": 12, "character": 15}, 340 | } 341 | }, 342 | { 343 | "range": { 344 | "start": {"line": 27, "character": 11}, 345 | "end": {"line": 27, "character": 20}, 346 | } 347 | }, 348 | ], 349 | ), 350 | ( 351 | 0, 352 | {"line": 15, "character": 20}, 353 | [ 354 | { 355 | "range": { 356 | "start": {"line": 15, "character": 17}, 357 | "end": {"line": 15, "character": 21}, 358 | } 359 | }, 360 | { 361 | "range": { 362 | "start": {"line": 16, "character": 8}, 363 | "end": {"line": 16, "character": 12}, 364 | } 365 | }, 366 | ], 367 | ), 368 | ( 369 | 0, 370 | {"line": 16, "character": 17}, 371 | [ 372 | { 373 | "range": { 374 | "start": {"line": 16, "character": 13}, 375 | "end": {"line": 16, "character": 19}, 376 | } 377 | }, 378 | { 379 | "range": { 380 | "start": {"line": 20, "character": 36}, 381 | "end": {"line": 20, "character": 42}, 382 | } 383 | }, 384 | ], 385 | ), 386 | ( 387 | 0, 388 | {"line": 18, "character": 15}, 389 | [ 390 | { 391 | "range": { 392 | "start": {"line": 18, "character": 8}, 393 | "end": {"line": 18, "character": 20}, 394 | } 395 | }, 396 | { 397 | "range": { 398 | "start": {"line": 28, "character": 9}, 399 | "end": {"line": 28, "character": 21}, 400 | } 401 | }, 402 | ], 403 | ), 404 | # __file__ 405 | ( 406 | 0, 407 | {"line": 34, "character": 8}, 408 | [ 409 | { 410 | "range": { 411 | "start": {"line": 34, "character": 6}, 412 | "end": {"line": 34, "character": 14}, 413 | } 414 | }, 415 | ], 416 | ), 417 | # __package__ 418 | ( 419 | 0, 420 | {"line": 35, "character": 8}, 421 | [ 422 | { 423 | "range": { 424 | "start": {"line": 35, "character": 6}, 425 | "end": {"line": 35, "character": 17}, 426 | } 427 | }, 428 | ], 429 | ), 430 | # __doc__ 431 | ( 432 | 0, 433 | {"line": 36, "character": 8}, 434 | [ 435 | { 436 | "range": { 437 | "start": {"line": 36, "character": 6}, 438 | "end": {"line": 36, "character": 13}, 439 | } 440 | }, 441 | ], 442 | ), 443 | # __name__ 444 | ( 445 | 0, 446 | {"line": 37, "character": 8}, 447 | [ 448 | { 449 | "range": { 450 | "start": {"line": 37, "character": 6}, 451 | "end": {"line": 37, "character": 14}, 452 | } 453 | }, 454 | ], 455 | ), 456 | ( 457 | 1, 458 | {"line": 0, "character": 0}, 459 | [ 460 | { 461 | "range": { 462 | "start": {"line": 0, "character": 0}, 463 | "end": {"line": 0, "character": 13}, 464 | } 465 | } 466 | ], 467 | ), 468 | ], 469 | ) 470 | def test_highlighting_notebook(cell, position, expected): 471 | """Tests highlighting on import statement for notebooks. 472 | 473 | Test Data: tests/test_data/highlighting/highlighting_test1.ipynb 474 | """ 475 | with session.LspSession() as ls_session: 476 | ls_session.initialize() 477 | path = HIGHLIGHTING_TEST_ROOT / "highlighting_test1.ipynb" 478 | cell_uris = ls_session.open_notebook_document(path) 479 | actual = ls_session.text_document_highlight( 480 | { 481 | "textDocument": {"uri": cell_uris[cell]}, 482 | "position": position, 483 | } 484 | ) 485 | 486 | assert_that(actual, is_(expected)) 487 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_hover.py: -------------------------------------------------------------------------------- 1 | """Tests for hover requests.""" 2 | 3 | from hamcrest import assert_that, is_ 4 | 5 | from tests import TEST_DATA 6 | from tests.lsp_test_client import session 7 | from tests.lsp_test_client.utils import as_uri 8 | 9 | HOVER_TEST_ROOT = TEST_DATA / "hover" 10 | 11 | 12 | def test_hover_on_module(): 13 | """Tests hover on the name of a imported module. 14 | 15 | Test Data: tests/test_data/hover/hover_test1.py 16 | """ 17 | with session.LspSession() as ls_session: 18 | ls_session.initialize() 19 | uri = as_uri(HOVER_TEST_ROOT / "hover_test1.py") 20 | actual = ls_session.text_document_hover( 21 | { 22 | "textDocument": {"uri": uri}, 23 | "position": {"line": 2, "character": 12}, 24 | } 25 | ) 26 | 27 | expected = { 28 | "contents": { 29 | "kind": "markdown", 30 | "value": "```python\nmodule somemodule\n```\n---\nModule doc string for testing.", 31 | }, 32 | "range": { 33 | "start": {"line": 2, "character": 7}, 34 | "end": {"line": 2, "character": 17}, 35 | }, 36 | } 37 | assert_that(actual, is_(expected)) 38 | 39 | 40 | def test_hover_on_module_notebook(): 41 | """Tests hover on the name of an imported module in a notebook. 42 | 43 | Test Data: tests/test_data/hover/hover_test1.ipynb 44 | """ 45 | with session.LspSession() as ls_session: 46 | ls_session.initialize() 47 | path = HOVER_TEST_ROOT / "hover_test1.ipynb" 48 | cell_uris = ls_session.open_notebook_document(path) 49 | 50 | actual = ls_session.text_document_hover( 51 | { 52 | "textDocument": {"uri": cell_uris[0]}, 53 | "position": {"line": 0, "character": 12}, 54 | } 55 | ) 56 | 57 | expected = { 58 | "contents": { 59 | "kind": "markdown", 60 | "value": "```python\nmodule somemodule\n```\n---\nModule doc string for testing.", 61 | }, 62 | "range": { 63 | "start": {"line": 0, "character": 7}, 64 | "end": {"line": 0, "character": 17}, 65 | }, 66 | } 67 | assert_that(actual, is_(expected)) 68 | 69 | 70 | def test_hover_on_function(): 71 | """Tests hover on the name of a function. 72 | 73 | Test Data: tests/test_data/hover/hover_test1.py 74 | """ 75 | with session.LspSession() as ls_session: 76 | ls_session.initialize() 77 | uri = as_uri(HOVER_TEST_ROOT / "hover_test1.py") 78 | actual = ls_session.text_document_hover( 79 | { 80 | "textDocument": {"uri": uri}, 81 | "position": {"line": 4, "character": 19}, 82 | } 83 | ) 84 | 85 | expected = { 86 | "contents": { 87 | "kind": "markdown", 88 | "value": "```python\ndef do_something()\n```\n---\nFunction doc string for testing.\n**Full name:** `somemodule.do_something`", 89 | }, 90 | "range": { 91 | "start": {"line": 4, "character": 11}, 92 | "end": {"line": 4, "character": 23}, 93 | }, 94 | } 95 | assert_that(actual, is_(expected)) 96 | 97 | 98 | def test_hover_on_function_notebook(): 99 | """Tests hover on the name of a function in a notebook. 100 | 101 | Test Data: tests/test_data/hover/hover_test1.ipynb 102 | """ 103 | with session.LspSession() as ls_session: 104 | ls_session.initialize() 105 | path = HOVER_TEST_ROOT / "hover_test1.ipynb" 106 | cell_uris = ls_session.open_notebook_document(path) 107 | 108 | actual = ls_session.text_document_hover( 109 | { 110 | "textDocument": {"uri": cell_uris[1]}, 111 | "position": {"line": 0, "character": 19}, 112 | } 113 | ) 114 | 115 | expected = { 116 | "contents": { 117 | "kind": "markdown", 118 | "value": "```python\ndef do_something()\n```\n---\nFunction doc string for testing.\n**Full name:** `somemodule.do_something`", 119 | }, 120 | "range": { 121 | "start": {"line": 0, "character": 11}, 122 | "end": {"line": 0, "character": 23}, 123 | }, 124 | } 125 | assert_that(actual, is_(expected)) 126 | 127 | 128 | def test_hover_on_class(): 129 | """Tests hover on the name of a class. 130 | 131 | Test Data: tests/test_data/hover/hover_test1.py 132 | """ 133 | with session.LspSession() as ls_session: 134 | ls_session.initialize() 135 | uri = as_uri(HOVER_TEST_ROOT / "hover_test1.py") 136 | actual = ls_session.text_document_hover( 137 | { 138 | "textDocument": {"uri": uri}, 139 | "position": {"line": 6, "character": 21}, 140 | } 141 | ) 142 | 143 | expected = { 144 | "contents": { 145 | "kind": "markdown", 146 | "value": "```python\nclass SomeClass()\n```\n---\nClass doc string for testing.\n**Full name:** `somemodule.SomeClass`", 147 | }, 148 | "range": { 149 | "start": {"line": 6, "character": 15}, 150 | "end": {"line": 6, "character": 24}, 151 | }, 152 | } 153 | assert_that(actual, is_(expected)) 154 | 155 | 156 | def test_hover_on_class_notebook(): 157 | """Tests hover on the name of a class in a notebook. 158 | 159 | Test Data: tests/test_data/hover/hover_test1.ipynb 160 | """ 161 | with session.LspSession() as ls_session: 162 | ls_session.initialize() 163 | path = HOVER_TEST_ROOT / "hover_test1.ipynb" 164 | cell_uris = ls_session.open_notebook_document(path) 165 | 166 | actual = ls_session.text_document_hover( 167 | { 168 | "textDocument": {"uri": cell_uris[2]}, 169 | "position": {"line": 0, "character": 21}, 170 | } 171 | ) 172 | 173 | expected = { 174 | "contents": { 175 | "kind": "markdown", 176 | "value": "```python\nclass SomeClass()\n```\n---\nClass doc string for testing.\n**Full name:** `somemodule.SomeClass`", 177 | }, 178 | "range": { 179 | "start": {"line": 0, "character": 15}, 180 | "end": {"line": 0, "character": 24}, 181 | }, 182 | } 183 | assert_that(actual, is_(expected)) 184 | 185 | 186 | def test_hover_on_method(): 187 | """Tests hover on the name of a class method. 188 | 189 | Test Data: tests/test_data/hover/hover_test1.py 190 | """ 191 | with session.LspSession() as ls_session: 192 | ls_session.initialize() 193 | uri = as_uri(HOVER_TEST_ROOT / "hover_test1.py") 194 | actual = ls_session.text_document_hover( 195 | { 196 | "textDocument": {"uri": uri}, 197 | "position": {"line": 8, "character": 6}, 198 | } 199 | ) 200 | 201 | expected = { 202 | "contents": { 203 | "kind": "markdown", 204 | "value": "```python\ndef some_method()\n```\n---\nMethod doc string for testing.\n**Full name:** `somemodule.SomeClass.some_method`", 205 | }, 206 | "range": { 207 | "start": {"line": 8, "character": 2}, 208 | "end": {"line": 8, "character": 13}, 209 | }, 210 | } 211 | assert_that(actual, is_(expected)) 212 | 213 | 214 | def test_hover_on_method_notebook(): 215 | """Tests hover on the name of a class method in a notebook. 216 | 217 | Test Data: tests/test_data/hover/hover_test1.ipynb 218 | """ 219 | with session.LspSession() as ls_session: 220 | ls_session.initialize() 221 | path = HOVER_TEST_ROOT / "hover_test1.ipynb" 222 | cell_uris = ls_session.open_notebook_document(path) 223 | 224 | actual = ls_session.text_document_hover( 225 | { 226 | "textDocument": {"uri": cell_uris[3]}, 227 | "position": {"line": 0, "character": 6}, 228 | } 229 | ) 230 | 231 | expected = { 232 | "contents": { 233 | "kind": "markdown", 234 | "value": "```python\ndef some_method()\n```\n---\nMethod doc string for testing.\n**Full name:** `somemodule.SomeClass.some_method`", 235 | }, 236 | "range": { 237 | "start": {"line": 0, "character": 2}, 238 | "end": {"line": 0, "character": 13}, 239 | }, 240 | } 241 | assert_that(actual, is_(expected)) 242 | 243 | 244 | def test_hover_on_method_no_docstring(): 245 | """Tests hover on the name of a class method without doc string. 246 | 247 | Test Data: tests/test_data/hover/hover_test1.py 248 | """ 249 | with session.LspSession() as ls_session: 250 | ls_session.initialize() 251 | uri = as_uri(HOVER_TEST_ROOT / "hover_test1.py") 252 | actual = ls_session.text_document_hover( 253 | { 254 | "textDocument": {"uri": uri}, 255 | "position": {"line": 10, "character": 6}, 256 | } 257 | ) 258 | 259 | expected = { 260 | "contents": { 261 | "kind": "markdown", 262 | "value": "```python\ndef some_method2()\n```\n---\n**Full name:** `somemodule.SomeClass.some_method2`", 263 | }, 264 | "range": { 265 | "start": {"line": 10, "character": 2}, 266 | "end": {"line": 10, "character": 14}, 267 | }, 268 | } 269 | assert_that(actual, is_(expected)) 270 | 271 | 272 | def test_hover_on_method_no_docstring_notebook(): 273 | """Tests hover on the name of a class method without doc string in a notebook. 274 | 275 | Test Data: tests/test_data/hover/hover_test1.ipynb 276 | """ 277 | with session.LspSession() as ls_session: 278 | ls_session.initialize() 279 | path = HOVER_TEST_ROOT / "hover_test1.ipynb" 280 | cell_uris = ls_session.open_notebook_document(path) 281 | 282 | actual = ls_session.text_document_hover( 283 | { 284 | "textDocument": {"uri": cell_uris[4]}, 285 | "position": {"line": 0, "character": 6}, 286 | } 287 | ) 288 | 289 | expected = { 290 | "contents": { 291 | "kind": "markdown", 292 | "value": "```python\ndef some_method2()\n```\n---\n**Full name:** `somemodule.SomeClass.some_method2`", 293 | }, 294 | "range": { 295 | "start": {"line": 0, "character": 2}, 296 | "end": {"line": 0, "character": 14}, 297 | }, 298 | } 299 | assert_that(actual, is_(expected)) 300 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_initialize.py: -------------------------------------------------------------------------------- 1 | """Tests for initialize requests.""" 2 | 3 | import copy 4 | from threading import Event 5 | 6 | from hamcrest import assert_that, is_ 7 | 8 | from tests.lsp_test_client import defaults, session 9 | 10 | 11 | def test_invalid_initialization_options() -> None: 12 | """Test what happens when invalid initialization is sent.""" 13 | initialize_params = copy.deepcopy(defaults.VSCODE_DEFAULT_INITIALIZE) 14 | initialize_params["initializationOptions"]["diagnostics"] = 1 15 | 16 | with session.LspSession() as ls_session: 17 | window_show_message_done = Event() 18 | window_log_message_done = Event() 19 | 20 | actual = [] 21 | 22 | def _window_show_message_handler(params): 23 | actual.append(params) 24 | window_show_message_done.set() 25 | 26 | ls_session.set_notification_callback( 27 | session.WINDOW_SHOW_MESSAGE, _window_show_message_handler 28 | ) 29 | 30 | def _window_log_message_handler(params): 31 | actual.append(params) 32 | window_log_message_done.set() 33 | 34 | ls_session.set_notification_callback( 35 | session.WINDOW_LOG_MESSAGE, _window_log_message_handler 36 | ) 37 | 38 | ls_session.initialize(initialize_params) 39 | 40 | window_show_message_done.wait(5) 41 | window_log_message_done.wait(5) 42 | 43 | assert_that(len(actual) == 2) 44 | for params in actual: 45 | assert_that(params["type"] == 1) 46 | assert_that( 47 | params["message"] 48 | == ( 49 | "Invalid InitializationOptions, using defaults: " 50 | "['invalid value for type, expected Diagnostics @ " 51 | "$.diagnostics']" 52 | ) 53 | ) 54 | 55 | 56 | def test_notebook_server_capabilities() -> None: 57 | """Test that the server's notebook capabilities are set correctly.""" 58 | actual = [] 59 | 60 | def _process_server_capabilities(capabilities): 61 | actual.append(capabilities) 62 | 63 | initialize_params = copy.deepcopy(defaults.VSCODE_DEFAULT_INITIALIZE) 64 | initialize_params["capabilities"]["notebookDocument"] = { 65 | "synchronization": { 66 | "dynamicRegistration": True, 67 | "executionSummarySupport": True, 68 | }, 69 | } 70 | with session.LspSession() as ls_session: 71 | ls_session.initialize(initialize_params, _process_server_capabilities) 72 | 73 | assert_that(len(actual) == 1) 74 | for params in actual: 75 | assert_that( 76 | params["capabilities"]["notebookDocumentSync"], 77 | is_( 78 | {"notebookSelector": [{"cells": [{"language": "python"}]}]} 79 | ), 80 | ) 81 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_refactoring.py: -------------------------------------------------------------------------------- 1 | """Tests for refactoring requests.""" 2 | 3 | import sys 4 | 5 | import pytest 6 | from hamcrest import assert_that, is_ 7 | 8 | from tests import TEST_DATA 9 | from tests.lsp_test_client import session 10 | from tests.lsp_test_client.utils import StringPattern, as_uri 11 | 12 | REFACTOR_TEST_ROOT = TEST_DATA / "refactoring" 13 | 14 | 15 | def test_lsp_rename_function(): 16 | """Tests single file function rename.""" 17 | with session.LspSession() as ls_session: 18 | ls_session.initialize() 19 | uri = as_uri((REFACTOR_TEST_ROOT / "rename_test1.py")) 20 | actual = ls_session.text_document_rename( 21 | { 22 | "textDocument": {"uri": uri}, 23 | "position": {"line": 12, "character": 4}, 24 | "newName": "my_function_1", 25 | } 26 | ) 27 | 28 | expected = { 29 | "documentChanges": [ 30 | { 31 | "textDocument": { 32 | "uri": uri, 33 | "version": 0, 34 | }, 35 | "edits": [ 36 | { 37 | "range": { 38 | "start": {"line": 3, "character": 6}, 39 | "end": {"line": 3, "character": 6}, 40 | }, 41 | "newText": "_", 42 | }, 43 | { 44 | "range": { 45 | "start": {"line": 3, "character": 10}, 46 | "end": {"line": 3, "character": 10}, 47 | }, 48 | "newText": "tion_", 49 | }, 50 | { 51 | "range": { 52 | "start": {"line": 8, "character": 6}, 53 | "end": {"line": 8, "character": 6}, 54 | }, 55 | "newText": "_", 56 | }, 57 | { 58 | "range": { 59 | "start": {"line": 8, "character": 10}, 60 | "end": {"line": 8, "character": 10}, 61 | }, 62 | "newText": "tion_", 63 | }, 64 | { 65 | "range": { 66 | "start": {"line": 12, "character": 2}, 67 | "end": {"line": 12, "character": 2}, 68 | }, 69 | "newText": "_", 70 | }, 71 | { 72 | "range": { 73 | "start": {"line": 12, "character": 6}, 74 | "end": {"line": 12, "character": 6}, 75 | }, 76 | "newText": "tion_", 77 | }, 78 | ], 79 | } 80 | ], 81 | } 82 | assert_that(actual, is_(expected)) 83 | 84 | 85 | def test_lsp_rename_function_notebook(): 86 | """Tests single notebook function rename.""" 87 | with session.LspSession() as ls_session: 88 | ls_session.initialize() 89 | path = REFACTOR_TEST_ROOT / "rename_test1.ipynb" 90 | cell_uris = ls_session.open_notebook_document(path) 91 | actual = ls_session.text_document_rename( 92 | { 93 | "textDocument": {"uri": cell_uris[2]}, 94 | "position": {"line": 0, "character": 4}, 95 | "newName": "my_function_1", 96 | } 97 | ) 98 | 99 | expected = { 100 | "documentChanges": [ 101 | { 102 | "textDocument": { 103 | "uri": cell_uris[0], 104 | "version": 1, 105 | }, 106 | "edits": [ 107 | { 108 | "range": { 109 | "start": {"line": 0, "character": 6}, 110 | "end": {"line": 0, "character": 6}, 111 | }, 112 | "newText": "_", 113 | }, 114 | { 115 | "range": { 116 | "start": {"line": 0, "character": 10}, 117 | "end": {"line": 0, "character": 10}, 118 | }, 119 | "newText": "tion_", 120 | }, 121 | ], 122 | }, 123 | { 124 | "textDocument": { 125 | "uri": cell_uris[1], 126 | "version": 1, 127 | }, 128 | "edits": [ 129 | { 130 | "range": { 131 | "start": {"line": 1, "character": 6}, 132 | "end": {"line": 1, "character": 6}, 133 | }, 134 | "newText": "_", 135 | }, 136 | { 137 | "range": { 138 | "start": {"line": 1, "character": 10}, 139 | "end": {"line": 1, "character": 10}, 140 | }, 141 | "newText": "tion_", 142 | }, 143 | ], 144 | }, 145 | { 146 | "textDocument": { 147 | "uri": cell_uris[2], 148 | "version": 1, 149 | }, 150 | "edits": [ 151 | { 152 | "range": { 153 | "start": {"line": 0, "character": 2}, 154 | "end": {"line": 0, "character": 2}, 155 | }, 156 | "newText": "_", 157 | }, 158 | { 159 | "range": { 160 | "start": {"line": 0, "character": 6}, 161 | "end": {"line": 0, "character": 6}, 162 | }, 163 | "newText": "tion_", 164 | }, 165 | ], 166 | }, 167 | ], 168 | } 169 | assert_that(actual, is_(expected)) 170 | 171 | 172 | def test_lsp_rename_variable_at_line_start(): 173 | """Tests renaming a variable that appears at the start of a line.""" 174 | with session.LspSession() as ls_session: 175 | ls_session.initialize() 176 | uri = as_uri((REFACTOR_TEST_ROOT / "rename_test2.py")) 177 | actual = ls_session.text_document_rename( 178 | { 179 | "textDocument": {"uri": uri}, 180 | "position": {"line": 1, "character": 0}, 181 | "newName": "y", 182 | } 183 | ) 184 | 185 | expected = { 186 | "documentChanges": [ 187 | { 188 | "textDocument": { 189 | "uri": uri, 190 | "version": 0, 191 | }, 192 | "edits": [ 193 | { 194 | "range": { 195 | "start": {"line": 1, "character": 0}, 196 | "end": {"line": 1, "character": 1}, 197 | }, 198 | "newText": "y", 199 | }, 200 | ], 201 | } 202 | ], 203 | } 204 | assert_that(actual, is_(expected)) 205 | 206 | 207 | def test_lsp_rename_inserts_at_line_start(): 208 | """Tests renaming a variable by inserting text at the start of a line.""" 209 | with session.LspSession() as ls_session: 210 | ls_session.initialize() 211 | uri = as_uri((REFACTOR_TEST_ROOT / "rename_test2.py")) 212 | actual = ls_session.text_document_rename( 213 | { 214 | "textDocument": {"uri": uri}, 215 | "position": {"line": 1, "character": 0}, 216 | # old name is "x", so we will insert "a" 217 | "newName": "ax", 218 | } 219 | ) 220 | 221 | expected = { 222 | "documentChanges": [ 223 | { 224 | "textDocument": { 225 | "uri": uri, 226 | "version": 0, 227 | }, 228 | "edits": [ 229 | { 230 | "range": { 231 | "start": {"line": 1, "character": 0}, 232 | "end": {"line": 1, "character": 0}, 233 | }, 234 | "newText": "a", 235 | }, 236 | ], 237 | } 238 | ], 239 | } 240 | assert_that(actual, is_(expected)) 241 | 242 | 243 | def test_lsp_rename_last_line(): 244 | """Tests whether rename works for end of file edge case. 245 | 246 | This example was receiving a KeyError, but now we check for end-1 to 247 | fit within correct range. 248 | """ 249 | with session.LspSession() as ls_session: 250 | ls_session.initialize() 251 | uri = as_uri((REFACTOR_TEST_ROOT / "rename_test3.py")) 252 | actual = ls_session.text_document_rename( 253 | { 254 | "textDocument": {"uri": uri}, 255 | "position": {"line": 14, "character": 7}, 256 | "newName": "args2", 257 | } 258 | ) 259 | 260 | expected = { 261 | "documentChanges": [ 262 | { 263 | "textDocument": { 264 | "uri": uri, 265 | "version": 0, 266 | }, 267 | "edits": [ 268 | { 269 | "range": { 270 | "start": {"line": 11, "character": 4}, 271 | "end": {"line": 11, "character": 4}, 272 | }, 273 | "newText": "2", 274 | }, 275 | { 276 | "range": { 277 | "start": {"line": 12, "character": 7}, 278 | "end": {"line": 12, "character": 7}, 279 | }, 280 | "newText": "2", 281 | }, 282 | { 283 | "range": { 284 | "start": {"line": 12, "character": 15}, 285 | "end": {"line": 12, "character": 15}, 286 | }, 287 | "newText": "2", 288 | }, 289 | { 290 | "range": { 291 | "start": {"line": 14, "character": 10}, 292 | "end": {"line": 14, "character": 12}, 293 | }, 294 | "newText": "2)\n", 295 | }, 296 | ], 297 | } 298 | ], 299 | } 300 | assert_that(actual, is_(expected)) 301 | 302 | 303 | def test_rename_package() -> None: 304 | """Tests renaming of an imported package.""" 305 | test_root = REFACTOR_TEST_ROOT / "rename_package_test1" 306 | with session.LspSession() as ls_session: 307 | ls_session.initialize() 308 | uri = as_uri(test_root / "rename_test_main.py") 309 | actual = ls_session.text_document_rename( 310 | { 311 | "textDocument": {"uri": uri}, 312 | "position": {"line": 2, "character": 12}, 313 | "newName": "new_name", 314 | } 315 | ) 316 | old_name_uri = as_uri(test_root / "old_name") 317 | new_name_uri = as_uri(test_root / "new_name") 318 | 319 | expected = { 320 | "documentChanges": [ 321 | { 322 | "textDocument": { 323 | "uri": uri, 324 | "version": 0, 325 | }, 326 | "edits": [ 327 | { 328 | "range": { 329 | "start": {"line": 2, "character": 5}, 330 | "end": {"line": 2, "character": 8}, 331 | }, 332 | "newText": "new", 333 | } 334 | ], 335 | }, 336 | { 337 | "kind": "rename", 338 | "oldUri": old_name_uri, 339 | "newUri": new_name_uri, 340 | "options": {"overwrite": True, "ignoreIfExists": True}, 341 | }, 342 | ] 343 | } 344 | assert_that(actual, is_(expected)) 345 | 346 | 347 | @pytest.mark.skipif( 348 | sys.platform == "win32", 349 | reason="Fails on Windows due to how pygls handles line endings " 350 | "(https://github.com/pappasam/jedi-language-server/issues/159)", 351 | ) 352 | def test_rename_package_notebook() -> None: 353 | """Tests renaming of an imported package in a notebook.""" 354 | test_root = REFACTOR_TEST_ROOT / "rename_package_test1" 355 | with session.LspSession() as ls_session: 356 | ls_session.initialize() 357 | path = test_root / "rename_test_main.ipynb" 358 | cell_uris = ls_session.open_notebook_document(path) 359 | actual = ls_session.text_document_rename( 360 | { 361 | "textDocument": {"uri": cell_uris[0]}, 362 | "position": {"line": 0, "character": 12}, 363 | "newName": "new_name", 364 | } 365 | ) 366 | old_name_uri = as_uri(test_root / "old_name") 367 | new_name_uri = as_uri(test_root / "new_name") 368 | 369 | expected = { 370 | "documentChanges": [ 371 | { 372 | "textDocument": { 373 | "uri": cell_uris[0], 374 | "version": 1, 375 | }, 376 | "edits": [ 377 | { 378 | "range": { 379 | "start": {"line": 0, "character": 5}, 380 | "end": {"line": 0, "character": 8}, 381 | }, 382 | "newText": "new", 383 | } 384 | ], 385 | }, 386 | { 387 | "textDocument": { 388 | "uri": as_uri(test_root / "rename_test_main.py"), 389 | "version": 0, 390 | }, 391 | "edits": [ 392 | { 393 | "range": { 394 | "start": {"line": 2, "character": 5}, 395 | "end": {"line": 2, "character": 8}, 396 | }, 397 | "newText": "new", 398 | }, 399 | ], 400 | }, 401 | { 402 | "kind": "rename", 403 | "oldUri": old_name_uri, 404 | "newUri": new_name_uri, 405 | "options": {"overwrite": True, "ignoreIfExists": True}, 406 | }, 407 | ] 408 | } 409 | assert_that(actual, is_(expected)) 410 | 411 | 412 | def test_rename_module() -> None: 413 | """Tests example from the following example. 414 | 415 | https://github.com/pappasam/jedi-language-server/issues/159 416 | """ 417 | test_root = REFACTOR_TEST_ROOT 418 | with session.LspSession() as ls_session: 419 | ls_session.initialize() 420 | uri = as_uri(test_root / "rename_module.py") 421 | actual = ls_session.text_document_rename( 422 | { 423 | "textDocument": {"uri": uri}, 424 | "position": {"line": 0, "character": 24}, 425 | "newName": "new_somemodule", 426 | } 427 | ) 428 | old_name_uri = as_uri(test_root / "somepackage" / "somemodule.py") 429 | new_name_uri = as_uri(test_root / "somepackage" / "new_somemodule.py") 430 | 431 | expected = { 432 | "documentChanges": [ 433 | { 434 | "textDocument": { 435 | "uri": uri, 436 | "version": 0, 437 | }, 438 | "edits": [ 439 | { 440 | "range": { 441 | "start": {"line": 0, "character": 24}, 442 | "end": {"line": 0, "character": 24}, 443 | }, 444 | "newText": "new_", 445 | }, 446 | { 447 | "range": { 448 | "start": {"line": 4, "character": 4}, 449 | "end": {"line": 4, "character": 4}, 450 | }, 451 | "newText": "new_", 452 | }, 453 | ], 454 | }, 455 | { 456 | "kind": "rename", 457 | "oldUri": old_name_uri, 458 | "newUri": new_name_uri, 459 | "options": {"overwrite": True, "ignoreIfExists": True}, 460 | }, 461 | ] 462 | } 463 | assert_that(actual, is_(expected)) 464 | 465 | 466 | def test_lsp_code_action() -> None: 467 | """Tests code actions like extract variable and extract function.""" 468 | with session.LspSession() as ls_session: 469 | ls_session.initialize() 470 | uri = as_uri((REFACTOR_TEST_ROOT / "code_action_test1.py")) 471 | actual = ls_session.text_document_code_action( 472 | { 473 | "textDocument": {"uri": uri}, 474 | "range": { 475 | "start": {"line": 4, "character": 10}, 476 | "end": {"line": 4, "character": 10}, 477 | }, 478 | "context": {"diagnostics": []}, 479 | } 480 | ) 481 | 482 | expected = [ 483 | { 484 | "title": StringPattern( 485 | r"Extract expression into variable 'jls_extract_var'" 486 | ), 487 | "kind": "refactor.extract", 488 | "edit": { 489 | "documentChanges": [ 490 | { 491 | "textDocument": { 492 | "uri": uri, 493 | "version": 0, 494 | }, 495 | "edits": [], 496 | } 497 | ] 498 | }, 499 | }, 500 | { 501 | "title": StringPattern( 502 | r"Extract expression into function 'jls_extract_def'" 503 | ), 504 | "kind": "refactor.extract", 505 | "edit": { 506 | "documentChanges": [ 507 | { 508 | "textDocument": { 509 | "uri": uri, 510 | "version": 0, 511 | }, 512 | "edits": [], 513 | } 514 | ] 515 | }, 516 | }, 517 | ] 518 | 519 | # Cannot use hamcrest directly for this due to unpredictable 520 | # variations in how the text edits are generated. 521 | 522 | assert_that(len(actual), is_(len(expected))) 523 | 524 | # Remove the edits 525 | actual[0]["edit"]["documentChanges"][0]["edits"] = [] 526 | actual[1]["edit"]["documentChanges"][0]["edits"] = [] 527 | 528 | assert_that(actual, is_(expected)) 529 | 530 | 531 | def test_lsp_code_action_notebook() -> None: 532 | """Tests code actions like extract variable and extract function in notebooks.""" 533 | with session.LspSession() as ls_session: 534 | ls_session.initialize() 535 | path = REFACTOR_TEST_ROOT / "code_action_test1.ipynb" 536 | cell_uris = ls_session.open_notebook_document(path) 537 | uri = cell_uris[0] 538 | actual = ls_session.text_document_code_action( 539 | { 540 | "textDocument": {"uri": uri}, 541 | "range": { 542 | "start": {"line": 4, "character": 10}, 543 | "end": {"line": 4, "character": 10}, 544 | }, 545 | "context": {"diagnostics": []}, 546 | } 547 | ) 548 | 549 | # Code actions are not yet supported in notebooks. 550 | assert_that(actual, is_(None)) 551 | 552 | 553 | def test_lsp_code_action2() -> None: 554 | """Tests edge case for code actions. 555 | 556 | Identified in: https://github.com/pappasam/jedi-language-server/issues/96 557 | """ 558 | with session.LspSession() as ls_session: 559 | ls_session.initialize() 560 | uri = as_uri((REFACTOR_TEST_ROOT / "code_action_test2.py")) 561 | actual = ls_session.text_document_code_action( 562 | { 563 | "textDocument": {"uri": uri}, 564 | "range": { 565 | "start": {"line": 2, "character": 6}, 566 | "end": {"line": 2, "character": 6}, 567 | }, 568 | "context": {"diagnostics": []}, 569 | } 570 | ) 571 | assert_that(actual, is_(None)) 572 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_references.py: -------------------------------------------------------------------------------- 1 | """Tests for references requests.""" 2 | 3 | import copy 4 | 5 | import pytest 6 | from hamcrest import assert_that, is_ 7 | 8 | from tests import TEST_DATA 9 | from tests.lsp_test_client import session 10 | from tests.lsp_test_client.defaults import VSCODE_DEFAULT_INITIALIZE 11 | from tests.lsp_test_client.utils import as_uri 12 | 13 | REFERENCES_TEST_ROOT = TEST_DATA / "references" 14 | 15 | 16 | references1 = as_uri(REFERENCES_TEST_ROOT / "references_test1.py") 17 | 18 | 19 | @pytest.mark.parametrize( 20 | ["position", "expected"], 21 | [ 22 | # from 23 | ({"line": 2, "character": 3}, None), 24 | # SOME_CONSTANT 25 | ( 26 | {"line": 4, "character": 8}, 27 | [ 28 | { 29 | "uri": references1, 30 | "range": { 31 | "start": {"line": 4, "character": 0}, 32 | "end": {"line": 4, "character": 13}, 33 | }, 34 | }, 35 | { 36 | "uri": references1, 37 | "range": { 38 | "start": {"line": 20, "character": 15}, 39 | "end": {"line": 20, "character": 28}, 40 | }, 41 | }, 42 | { 43 | "uri": references1, 44 | "range": { 45 | "start": {"line": 24, "character": 29}, 46 | "end": {"line": 24, "character": 42}, 47 | }, 48 | }, 49 | ], 50 | ), 51 | # some_function 52 | ( 53 | {"line": 7, "character": 9}, 54 | [ 55 | { 56 | "uri": references1, 57 | "range": { 58 | "start": {"line": 7, "character": 4}, 59 | "end": {"line": 7, "character": 17}, 60 | }, 61 | }, 62 | { 63 | "uri": references1, 64 | "range": { 65 | "start": {"line": 24, "character": 15}, 66 | "end": {"line": 24, "character": 28}, 67 | }, 68 | }, 69 | ], 70 | ), 71 | # arg of some_function 72 | ( 73 | {"line": 7, "character": 20}, 74 | [ 75 | { 76 | "uri": references1, 77 | "range": { 78 | "start": {"line": 7, "character": 18}, 79 | "end": {"line": 7, "character": 21}, 80 | }, 81 | }, 82 | { 83 | "uri": references1, 84 | "range": { 85 | "start": {"line": 9, "character": 11}, 86 | "end": {"line": 9, "character": 14}, 87 | }, 88 | }, 89 | ], 90 | ), 91 | # SomeClass 92 | ( 93 | {"line": 12, "character": 14}, 94 | [ 95 | { 96 | "uri": references1, 97 | "range": { 98 | "start": {"line": 12, "character": 6}, 99 | "end": {"line": 12, "character": 15}, 100 | }, 101 | }, 102 | { 103 | "uri": references1, 104 | "range": { 105 | "start": {"line": 27, "character": 11}, 106 | "end": {"line": 27, "character": 20}, 107 | }, 108 | }, 109 | ], 110 | ), 111 | # self arg of SomeClass.__init__ 112 | ( 113 | {"line": 15, "character": 20}, 114 | [ 115 | { 116 | "uri": references1, 117 | "range": { 118 | "start": {"line": 15, "character": 17}, 119 | "end": {"line": 15, "character": 21}, 120 | }, 121 | }, 122 | { 123 | "uri": references1, 124 | "range": { 125 | "start": {"line": 16, "character": 8}, 126 | "end": {"line": 16, "character": 12}, 127 | }, 128 | }, 129 | ], 130 | ), 131 | # SomeClass._field 132 | ( 133 | {"line": 16, "character": 17}, 134 | [ 135 | { 136 | "uri": references1, 137 | "range": { 138 | "start": {"line": 16, "character": 13}, 139 | "end": {"line": 16, "character": 19}, 140 | }, 141 | }, 142 | { 143 | "uri": references1, 144 | "range": { 145 | "start": {"line": 20, "character": 36}, 146 | "end": {"line": 20, "character": 42}, 147 | }, 148 | }, 149 | ], 150 | ), 151 | # SomeClass.some_method1 152 | ( 153 | {"line": 18, "character": 15}, 154 | [ 155 | { 156 | "uri": references1, 157 | "range": { 158 | "start": {"line": 18, "character": 8}, 159 | "end": {"line": 18, "character": 20}, 160 | }, 161 | }, 162 | { 163 | "uri": references1, 164 | "range": { 165 | "start": {"line": 28, "character": 9}, 166 | "end": {"line": 28, "character": 21}, 167 | }, 168 | }, 169 | ], 170 | ), 171 | ], 172 | ) 173 | def test_references(position, expected): 174 | """Tests references on import statement. 175 | 176 | Test Data: tests/test_data/references/references_test1.py 177 | """ 178 | initialize_params = copy.deepcopy(VSCODE_DEFAULT_INITIALIZE) 179 | initialize_params["workspaceFolders"] = [ 180 | {"uri": as_uri(REFERENCES_TEST_ROOT), "name": "jedi_lsp"} 181 | ] 182 | initialize_params["rootPath"]: str(REFERENCES_TEST_ROOT) 183 | initialize_params["rootUri"]: as_uri(REFERENCES_TEST_ROOT) 184 | 185 | with session.LspSession() as ls_session: 186 | ls_session.initialize(initialize_params) 187 | uri = references1 188 | actual = ls_session.text_document_references( 189 | { 190 | "textDocument": {"uri": uri}, 191 | "position": position, 192 | "context": { 193 | "includeDeclaration": True, 194 | }, 195 | } 196 | ) 197 | 198 | assert_that(actual, is_(expected)) 199 | 200 | 201 | @pytest.mark.parametrize( 202 | ["cell", "position", "expected"], 203 | [ 204 | # from 205 | (0, {"line": 0, "character": 3}, None), 206 | # SOME_CONSTANT 207 | ( 208 | 1, 209 | {"line": 0, "character": 8}, 210 | [ 211 | { 212 | "cell": 1, 213 | "range": { 214 | "start": {"line": 0, "character": 0}, 215 | "end": {"line": 0, "character": 13}, 216 | }, 217 | }, 218 | { 219 | "cell": 3, 220 | "range": { 221 | "start": {"line": 8, "character": 15}, 222 | "end": {"line": 8, "character": 28}, 223 | }, 224 | }, 225 | { 226 | "cell": 3, 227 | "range": { 228 | "start": {"line": 12, "character": 29}, 229 | "end": {"line": 12, "character": 42}, 230 | }, 231 | }, 232 | ], 233 | ), 234 | # some_function 235 | ( 236 | 2, 237 | {"line": 0, "character": 9}, 238 | [ 239 | { 240 | "cell": 2, 241 | "range": { 242 | "start": {"line": 0, "character": 4}, 243 | "end": {"line": 0, "character": 17}, 244 | }, 245 | }, 246 | { 247 | "cell": 3, 248 | "range": { 249 | "start": {"line": 12, "character": 15}, 250 | "end": {"line": 12, "character": 28}, 251 | }, 252 | }, 253 | ], 254 | ), 255 | # arg of some_function 256 | ( 257 | 2, 258 | {"line": 0, "character": 20}, 259 | [ 260 | { 261 | "cell": 2, 262 | "range": { 263 | "start": {"line": 0, "character": 18}, 264 | "end": {"line": 0, "character": 21}, 265 | }, 266 | }, 267 | { 268 | "cell": 2, 269 | "range": { 270 | "start": {"line": 2, "character": 11}, 271 | "end": {"line": 2, "character": 14}, 272 | }, 273 | }, 274 | ], 275 | ), 276 | # SomeClass 277 | ( 278 | 3, 279 | {"line": 0, "character": 14}, 280 | [ 281 | { 282 | "cell": 3, 283 | "range": { 284 | "start": {"line": 0, "character": 6}, 285 | "end": {"line": 0, "character": 15}, 286 | }, 287 | }, 288 | { 289 | "cell": 4, 290 | "range": { 291 | "start": {"line": 0, "character": 11}, 292 | "end": {"line": 0, "character": 20}, 293 | }, 294 | }, 295 | ], 296 | ), 297 | # self arg of SomeClass.__init__ 298 | ( 299 | 3, 300 | {"line": 3, "character": 20}, 301 | [ 302 | { 303 | "cell": 3, 304 | "range": { 305 | "start": {"line": 3, "character": 17}, 306 | "end": {"line": 3, "character": 21}, 307 | }, 308 | }, 309 | { 310 | "cell": 3, 311 | "range": { 312 | "start": {"line": 4, "character": 8}, 313 | "end": {"line": 4, "character": 12}, 314 | }, 315 | }, 316 | ], 317 | ), 318 | # SomeClass._field 319 | ( 320 | 3, 321 | {"line": 4, "character": 17}, 322 | [ 323 | { 324 | "cell": 3, 325 | "range": { 326 | "start": {"line": 4, "character": 13}, 327 | "end": {"line": 4, "character": 19}, 328 | }, 329 | }, 330 | { 331 | "cell": 3, 332 | "range": { 333 | "start": {"line": 8, "character": 36}, 334 | "end": {"line": 8, "character": 42}, 335 | }, 336 | }, 337 | ], 338 | ), 339 | # SomeClass.some_method1 340 | ( 341 | 3, 342 | {"line": 6, "character": 15}, 343 | [ 344 | { 345 | "cell": 3, 346 | "range": { 347 | "start": {"line": 6, "character": 8}, 348 | "end": {"line": 6, "character": 20}, 349 | }, 350 | }, 351 | { 352 | "cell": 4, 353 | "range": { 354 | "start": {"line": 1, "character": 9}, 355 | "end": {"line": 1, "character": 21}, 356 | }, 357 | }, 358 | ], 359 | ), 360 | ], 361 | ) 362 | def test_references_notebook(cell, position, expected): 363 | """Tests references in a notebook. 364 | 365 | Test Data: tests/test_data/references/references_test1.ipynb 366 | """ 367 | with session.LspSession() as ls_session: 368 | ls_session.initialize() 369 | path = REFERENCES_TEST_ROOT / "references_test1.ipynb" 370 | cell_uris = ls_session.open_notebook_document(path) 371 | actual = ls_session.text_document_references( 372 | { 373 | "textDocument": {"uri": cell_uris[cell]}, 374 | "position": position, 375 | "context": { 376 | "includeDeclaration": True, 377 | }, 378 | } 379 | ) 380 | 381 | if expected: 382 | for item in expected: 383 | item["uri"] = cell_uris[item.pop("cell")] 384 | assert_that(actual, is_(expected)) 385 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_semantic_tokens.py: -------------------------------------------------------------------------------- 1 | """Tests for semantic tokens requests.""" 2 | 3 | import copy 4 | 5 | import pylsp_jsonrpc 6 | from hamcrest import assert_that, calling, is_, raises 7 | 8 | from tests import TEST_DATA 9 | from tests.lsp_test_client import session 10 | from tests.lsp_test_client.defaults import VSCODE_DEFAULT_INITIALIZE 11 | from tests.lsp_test_client.utils import as_uri 12 | 13 | SEMANTIC_TEST_ROOT = TEST_DATA / "semantic_tokens" 14 | 15 | 16 | def initialize_session(ls_session: session.LspSession): 17 | initialize_params = copy.deepcopy(VSCODE_DEFAULT_INITIALIZE) 18 | initialize_params["initializationOptions"] = { 19 | "semanticTokens": {"enable": True} 20 | } 21 | ls_session.initialize(initialize_params) 22 | 23 | 24 | def test_semantic_tokens_disabled(): 25 | with session.LspSession() as ls_session: 26 | ls_session.initialize() 27 | 28 | assert_that( 29 | calling(ls_session.text_doc_semantic_tokens_full).with_args( 30 | { 31 | "textDocument": {"uri": ""}, 32 | } 33 | ), 34 | raises(pylsp_jsonrpc.exceptions.JsonRpcException), 35 | ) 36 | 37 | 38 | def test_semantic_tokens_full_import(): 39 | """Tests tokens for 'import name1 as name2'. 40 | 41 | Test Data: tests/test_data/semantic_tokens/semantic_tokens_test1.py. 42 | """ 43 | with session.LspSession() as ls_session: 44 | initialize_session(ls_session) 45 | uri = as_uri(SEMANTIC_TEST_ROOT / "semantic_tokens_test1.py") 46 | actual = ls_session.text_doc_semantic_tokens_full( 47 | { 48 | "textDocument": {"uri": uri}, 49 | } 50 | ) 51 | # fmt: off 52 | # [line, column, length, id, mod_id] 53 | expected = { 54 | "data": [ 55 | 5, 7, 2, 0, 0, # "import re" 56 | 1, 7, 3, 0, 0, # "import sys, " 57 | 0, 5, 2, 0, 0, # "os." 58 | 0, 3, 4, 0, 0, # "path as " 59 | 0, 8, 4, 0, 0, # "path" 60 | ] 61 | } 62 | # fmt: on 63 | assert_that(actual, is_(expected)) 64 | 65 | 66 | def test_semantic_tokens_full_import_from(): 67 | """Tests tokens for 'from name1 import name2 as name3'. 68 | 69 | Test Data: tests/test_data/semantic_tokens/semantic_tokens_test2.py. 70 | """ 71 | with session.LspSession() as ls_session: 72 | initialize_session(ls_session) 73 | uri = as_uri(SEMANTIC_TEST_ROOT / "semantic_tokens_test2.py") 74 | actual = ls_session.text_doc_semantic_tokens_full( 75 | { 76 | "textDocument": {"uri": uri}, 77 | } 78 | ) 79 | # fmt: off 80 | # [line, column, length, id, mod_id] 81 | expected = { 82 | "data": [ 83 | 0, 5, 2, 0, 0, # "from os." 84 | 0, 3, 4, 0, 0, # "path import " 85 | 0, 12, 6, 2, 0, # "exists" 86 | 1, 5, 3, 0, 0, # "from sys import" 87 | 0, 11, 4, 4, 0, # "argv as " 88 | 0, 8, 9, 4, 0, # "arguments" 89 | ] 90 | } 91 | # fmt: on 92 | assert_that(actual, is_(expected)) 93 | 94 | 95 | def test_semantic_tokens_range_import_from(): 96 | """Tests tokens for 'from name1 import name2 as name3'. 97 | 98 | Test Data: tests/test_data/semantic_tokens/semantic_tokens_test2.py. 99 | """ 100 | with session.LspSession() as ls_session: 101 | initialize_session(ls_session) 102 | uri = as_uri(SEMANTIC_TEST_ROOT / "semantic_tokens_test2.py") 103 | actual = ls_session.text_doc_semantic_tokens_range( 104 | { 105 | "textDocument": {"uri": uri}, 106 | "range": { 107 | "start": {"line": 1, "character": 1}, 108 | "end": {"line": 2, "character": 0}, 109 | }, 110 | } 111 | ) 112 | # fmt: off 113 | # [line, column, length, id, mod_id] 114 | expected = { 115 | "data": [ 116 | 0, 4, 3, 0, 0, # "from sys import" 117 | 0, 11, 4, 4, 0, # "argv as " 118 | 0, 8, 9, 4, 0, # "arguments" 119 | ] 120 | } 121 | # fmt: on 122 | assert_that(actual, is_(expected)) 123 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_signature.py: -------------------------------------------------------------------------------- 1 | """Tests for signature help requests.""" 2 | 3 | import pytest 4 | from hamcrest import assert_that, is_ 5 | 6 | from tests import TEST_DATA 7 | from tests.lsp_test_client import session 8 | from tests.lsp_test_client.utils import as_uri 9 | 10 | SIGNATURE_TEST_ROOT = TEST_DATA / "signature" 11 | 12 | 13 | @pytest.mark.parametrize( 14 | ["trigger_char", "column", "active_param"], [("(", 14, 0), (",", 18, 1)] 15 | ) 16 | def test_signature_help(trigger_char, column, active_param): 17 | """Tests signature help response for a function. 18 | 19 | Test Data: tests/test_data/signature/signature_test1.py 20 | """ 21 | with session.LspSession() as ls_session: 22 | ls_session.initialize() 23 | uri = as_uri(SIGNATURE_TEST_ROOT / "signature_test1.py") 24 | actual = ls_session.text_document_signature_help( 25 | { 26 | "textDocument": {"uri": uri}, 27 | "position": {"line": 7, "character": column}, 28 | "context": { 29 | "isRetrigger": False, 30 | "triggerCharacter": trigger_char, 31 | "triggerKind": 2, 32 | }, 33 | } 34 | ) 35 | 36 | expected = { 37 | "signatures": [ 38 | { 39 | "label": ( 40 | "def some_function(arg1: str, arg2: int, arg3: list)" 41 | ), 42 | "documentation": { 43 | "kind": "markdown", 44 | "value": "This is a test function.", 45 | }, 46 | "parameters": [ 47 | {"label": "arg1: str"}, 48 | {"label": "arg2: int"}, 49 | {"label": "arg3: list"}, 50 | ], 51 | } 52 | ], 53 | "activeSignature": 0, 54 | "activeParameter": active_param, 55 | } 56 | 57 | assert_that(actual, is_(expected)) 58 | 59 | 60 | @pytest.mark.parametrize( 61 | ["trigger_char", "column", "active_param"], [("(", 14, 0), (",", 18, 1)] 62 | ) 63 | def test_signature_help_notebook(trigger_char, column, active_param): 64 | """Tests signature help response for a function in a notebook. 65 | 66 | Test Data: tests/test_data/signature/signature_test1.ipynb 67 | """ 68 | with session.LspSession() as ls_session: 69 | ls_session.initialize() 70 | path = SIGNATURE_TEST_ROOT / "signature_test1.ipynb" 71 | cell_uris = ls_session.open_notebook_document(path) 72 | actual = ls_session.text_document_signature_help( 73 | { 74 | "textDocument": {"uri": cell_uris[1]}, 75 | "position": {"line": 0, "character": column}, 76 | "context": { 77 | "isRetrigger": False, 78 | "triggerCharacter": trigger_char, 79 | "triggerKind": 2, 80 | }, 81 | } 82 | ) 83 | 84 | expected = { 85 | "signatures": [ 86 | { 87 | "label": ( 88 | "def some_function(arg1: str, arg2: int, arg3: list)" 89 | ), 90 | "documentation": { 91 | "kind": "markdown", 92 | "value": "This is a test function.", 93 | }, 94 | "parameters": [ 95 | {"label": "arg1: str"}, 96 | {"label": "arg2: int"}, 97 | {"label": "arg3: list"}, 98 | ], 99 | } 100 | ], 101 | "activeSignature": 0, 102 | "activeParameter": active_param, 103 | } 104 | 105 | assert_that(actual, is_(expected)) 106 | -------------------------------------------------------------------------------- /tests/lsp_tests/test_workspace_symbol.py: -------------------------------------------------------------------------------- 1 | """Tests for workspace symbols requests.""" 2 | 3 | from hamcrest import assert_that, is_ 4 | 5 | from tests import TEST_DATA 6 | from tests.lsp_test_client import session 7 | from tests.lsp_test_client.utils import as_uri 8 | 9 | SYMBOL_TEST_ROOT = TEST_DATA / "symbol" 10 | 11 | 12 | def test_workspace_symbol() -> None: 13 | """Test workspace symbol request. 14 | 15 | Test Data: tests/test_data/symbol/somemodule2.py 16 | """ 17 | with session.LspSession() as ls_session: 18 | ls_session.initialize() 19 | actual = ls_session.workspace_symbol({"query": "do_workspace_thing"}) 20 | 21 | module_uri = as_uri(SYMBOL_TEST_ROOT / "somemodule2.py") 22 | expected = [ 23 | { 24 | "name": "do_workspace_thing", 25 | "kind": 12, 26 | "location": { 27 | "uri": module_uri, 28 | "range": { 29 | "start": {"line": 7, "character": 4}, 30 | "end": {"line": 7, "character": 22}, 31 | }, 32 | }, 33 | "containerName": ( 34 | "tests.test_data.symbol.somemodule2.do_workspace_thing" 35 | ), 36 | } 37 | ] 38 | assert_that(actual, is_(expected)) 39 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """Test the CLI.""" 2 | 3 | from jedi_language_server.cli import cli, get_version 4 | 5 | 6 | def test_get_version() -> None: 7 | """Test that the get_version function returns a string.""" 8 | assert isinstance(get_version(), str) 9 | 10 | 11 | def test_cli() -> None: 12 | """Test the basic cli behavior.""" 13 | assert cli 14 | -------------------------------------------------------------------------------- /tests/test_data/completion/completion_test1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "def my_function():\n", 10 | " \"\"\"Simple test function.\"\"\"\n", 11 | " return 1" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "my" 21 | ] 22 | } 23 | ], 24 | "metadata": { 25 | "language_info": { 26 | "name": "python" 27 | } 28 | }, 29 | "nbformat": 4, 30 | "nbformat_minor": 2 31 | } 32 | -------------------------------------------------------------------------------- /tests/test_data/completion/completion_test1.py: -------------------------------------------------------------------------------- 1 | """Test file for test_completion.""" 2 | 3 | 4 | def my_function(): 5 | """Simple test function.""" 6 | return 1 7 | 8 | 9 | my 10 | -------------------------------------------------------------------------------- /tests/test_data/completion/completion_test2.py: -------------------------------------------------------------------------------- 1 | """Test file for test_completion.""" 2 | 3 | 4 | class MyClass: 5 | """Simple class.""" 6 | 7 | 8 | MyC 9 | -------------------------------------------------------------------------------- /tests/test_data/completion/completion_test_class_self.py: -------------------------------------------------------------------------------- 1 | class SomeClass: 2 | def some_method(self, x): 3 | """Great method.""" 4 | return x 5 | 6 | 7 | instance = SomeClass() 8 | instance.some 9 | -------------------------------------------------------------------------------- /tests/test_data/definition/definition_test1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import somemodule, somemodule2" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "somemodule.some_function()\n", 19 | "somemodule2.some_function()" 20 | ] 21 | } 22 | ], 23 | "metadata": { 24 | "language_info": { 25 | "name": "python" 26 | } 27 | }, 28 | "nbformat": 4, 29 | "nbformat_minor": 2 30 | } 31 | -------------------------------------------------------------------------------- /tests/test_data/definition/definition_test1.py: -------------------------------------------------------------------------------- 1 | """Test file for definition tests.""" 2 | 3 | from . import somemodule, somemodule2 4 | 5 | somemodule.some_function() 6 | somemodule2.some_function() 7 | -------------------------------------------------------------------------------- /tests/test_data/definition/definition_test2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "def some_function():\n", 10 | " \"\"\"Some test function.\"\"\"" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "some_function()" 20 | ] 21 | } 22 | ], 23 | "metadata": { 24 | "language_info": { 25 | "name": "python" 26 | } 27 | }, 28 | "nbformat": 4, 29 | "nbformat_minor": 2 30 | } 31 | -------------------------------------------------------------------------------- /tests/test_data/definition/somemodule.py: -------------------------------------------------------------------------------- 1 | """Module for definition tests.""" 2 | 3 | 4 | def some_function(): 5 | """Some test function.""" 6 | -------------------------------------------------------------------------------- /tests/test_data/definition/somemodule2.py: -------------------------------------------------------------------------------- 1 | """Module for definition tests.""" 2 | 3 | 4 | def some_function(): 5 | """Some test function.""" 6 | -------------------------------------------------------------------------------- /tests/test_data/diagnostics/diagnostics_test1_content_changes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "textDocument": { 4 | "uri": "", 5 | "version": 1 6 | }, 7 | "contentChanges": [ 8 | { 9 | "range": { 10 | "start": { 11 | "line": 5, 12 | "character": 15 13 | }, 14 | "end": { 15 | "line": 5, 16 | "character": 15 17 | } 18 | }, 19 | "rangeLength": 0, 20 | "text": "=" 21 | } 22 | ] 23 | } 24 | ] -------------------------------------------------------------------------------- /tests/test_data/diagnostics/diagnostics_test1_contents.txt: -------------------------------------------------------------------------------- 1 | """Test data for problem diagnostics.""" 2 | 3 | 4 | def some_function(): 5 | """Function to test diagnostics.""" 6 | return 1 == 1 7 | -------------------------------------------------------------------------------- /tests/test_data/highlighting/highlighting_test1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "\"\"\"Test file for highlighting.\"\"\"\n", 10 | "\n", 11 | "from enum import Enum\n", 12 | "\n", 13 | "SOME_CONSTANT = \"something\"\n", 14 | "\n", 15 | "\n", 16 | "def some_function(arg: str) -> str:\n", 17 | " \"\"\"Test function for highlighting.\"\"\"\n", 18 | " return arg\n", 19 | "\n", 20 | "\n", 21 | "class SomeClass(Enum):\n", 22 | " \"\"\"Test class for highlighting.\"\"\"\n", 23 | "\n", 24 | " def __init__(self):\n", 25 | " self._field = 1\n", 26 | "\n", 27 | " def some_method1(self):\n", 28 | " \"\"\"Test method for highlighting.\"\"\"\n", 29 | " return SOME_CONSTANT + self._field\n", 30 | "\n", 31 | " def some_method2(self):\n", 32 | " \"\"\"Test method for highlighting.\"\"\"\n", 33 | " return some_function(SOME_CONSTANT)\n", 34 | "\n", 35 | "\n", 36 | "instance = SomeClass()\n", 37 | "instance.some_method1()\n", 38 | "instance.some_method2()\n", 39 | "\n", 40 | "# Jedi returns an implicit definition for these special variables,\n", 41 | "# which has no line number. Highlighting of usages with line numbers\n", 42 | "# should still function.\n", 43 | "print(__file__)\n", 44 | "print(__package__)\n", 45 | "print(__doc__)\n", 46 | "print(__name__)" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "SOME_CONSTANT" 56 | ] 57 | } 58 | ], 59 | "metadata": { 60 | "language_info": { 61 | "name": "python" 62 | } 63 | }, 64 | "nbformat": 4, 65 | "nbformat_minor": 2 66 | } 67 | -------------------------------------------------------------------------------- /tests/test_data/highlighting/highlighting_test1.py: -------------------------------------------------------------------------------- 1 | """Test file for highlighting.""" 2 | 3 | from enum import Enum 4 | 5 | SOME_CONSTANT = "something" 6 | 7 | 8 | def some_function(arg: str) -> str: 9 | """Test function for highlighting.""" 10 | return arg 11 | 12 | 13 | class SomeClass(Enum): 14 | """Test class for highlighting.""" 15 | 16 | def __init__(self): 17 | self._field = 1 18 | 19 | def some_method1(self): 20 | """Test method for highlighting.""" 21 | return SOME_CONSTANT + self._field 22 | 23 | def some_method2(self): 24 | """Test method for highlighting.""" 25 | return some_function(SOME_CONSTANT) 26 | 27 | 28 | instance = SomeClass() 29 | instance.some_method1() 30 | instance.some_method2() 31 | 32 | # Jedi returns an implicit definition for these special variables, 33 | # which has no line number. Highlighting of usages with line numbers 34 | # should still function. 35 | print(__file__) 36 | print(__package__) 37 | print(__doc__) 38 | print(__name__) 39 | -------------------------------------------------------------------------------- /tests/test_data/hover/hover_test1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import somemodule" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "somemodule.do_something()" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "c = somemodule.SomeClass()" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "c.some_method()" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "c.some_method2()" 46 | ] 47 | } 48 | ], 49 | "metadata": { 50 | "language_info": { 51 | "name": "python" 52 | } 53 | }, 54 | "nbformat": 4, 55 | "nbformat_minor": 2 56 | } 57 | -------------------------------------------------------------------------------- /tests/test_data/hover/hover_test1.py: -------------------------------------------------------------------------------- 1 | """Test data for hover.""" 2 | 3 | import somemodule 4 | 5 | somemodule.do_something() 6 | 7 | c = somemodule.SomeClass() 8 | 9 | c.some_method() 10 | 11 | c.some_method2() 12 | -------------------------------------------------------------------------------- /tests/test_data/hover/somemodule.py: -------------------------------------------------------------------------------- 1 | """Module doc string for testing.""" 2 | 3 | 4 | def do_something(): 5 | """Function doc string for testing.""" 6 | 7 | 8 | class SomeClass: 9 | """Class doc string for testing.""" 10 | 11 | def some_method(self): 12 | """Method doc string for testing.""" 13 | 14 | def some_method2(self): 15 | pass 16 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/code_action_test1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "\"\"\"Test file for code action tests in test_refactoring.\"\"\"\n", 10 | "\n", 11 | "\n", 12 | "def do_something(x):\n", 13 | " if x == 1:\n", 14 | " pass" 15 | ] 16 | } 17 | ], 18 | "metadata": { 19 | "language_info": { 20 | "name": "python" 21 | } 22 | }, 23 | "nbformat": 4, 24 | "nbformat_minor": 2 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/code_action_test1.py: -------------------------------------------------------------------------------- 1 | """Test file for code action tests in test_refactoring.""" 2 | 3 | 4 | def do_something(x): 5 | if x == 1: 6 | pass 7 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/code_action_test2.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | random.gauss 4 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/rename_module.py: -------------------------------------------------------------------------------- 1 | from somepackage import somemodule 2 | 3 | 4 | def run(): 5 | somemodule.bar() 6 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/rename_package_test1/old_name/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pappasam/jedi-language-server/63744d56926f197df5995b309b832c7d0e084526/tests/test_data/refactoring/rename_package_test1/old_name/__init__.py -------------------------------------------------------------------------------- /tests/test_data/refactoring/rename_package_test1/old_name/some_module.py: -------------------------------------------------------------------------------- 1 | """Test file for rename package tests in test_refactoring.""" 2 | 3 | 4 | def do_something() -> None: 5 | """Test function.""" 6 | print("do_something") 7 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/rename_package_test1/rename_test_main.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from old_name.some_module import do_something" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "do_something()" 19 | ] 20 | } 21 | ], 22 | "metadata": { 23 | "language_info": { 24 | "name": "python" 25 | } 26 | }, 27 | "nbformat": 4, 28 | "nbformat_minor": 2 29 | } 30 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/rename_package_test1/rename_test_main.py: -------------------------------------------------------------------------------- 1 | """Test file for rename package tests in test_refactoring.""" 2 | 3 | from old_name.some_module import do_something 4 | 5 | do_something() 6 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/rename_test1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "def myfunc1():\n", 10 | " print(\"myfunc1\")" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "def my_function_2():\n", 20 | " myfunc1()\n", 21 | " print(\"my_function_2\")" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "myfunc1()" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "my_function_2()" 40 | ] 41 | } 42 | ], 43 | "metadata": { 44 | "language_info": { 45 | "name": "python" 46 | } 47 | }, 48 | "nbformat": 4, 49 | "nbformat_minor": 2 50 | } 51 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/rename_test1.py: -------------------------------------------------------------------------------- 1 | """Test file for test_refactoring.""" 2 | 3 | 4 | def myfunc1(): 5 | print("myfunc1") 6 | 7 | 8 | def my_function_2(): 9 | myfunc1() 10 | print("my_function_2") 11 | 12 | 13 | myfunc1() 14 | my_function_2() 15 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/rename_test2.py: -------------------------------------------------------------------------------- 1 | # Variable to be renamed appears at the very start of the line. 2 | x = 3 3 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/rename_test3.py: -------------------------------------------------------------------------------- 1 | """Example argument parser for data science.""" 2 | 3 | 4 | def my_datetime(in_date: str) -> None: 5 | pass 6 | 7 | 8 | def csv_str(in_str: str) -> None: 9 | pass 10 | 11 | 12 | args = 12 13 | if args > (args + 2): 14 | print("oops") 15 | print(args) 16 | -------------------------------------------------------------------------------- /tests/test_data/refactoring/somepackage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pappasam/jedi-language-server/63744d56926f197df5995b309b832c7d0e084526/tests/test_data/refactoring/somepackage/__init__.py -------------------------------------------------------------------------------- /tests/test_data/refactoring/somepackage/somemodule.py: -------------------------------------------------------------------------------- 1 | def bar(): 2 | pass 3 | -------------------------------------------------------------------------------- /tests/test_data/references/references_test1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from enum import Enum" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "SOME_CONSTANT = \"something\"" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "def some_function(arg: str) -> str:\n", 28 | " \"\"\"Test function for references.\"\"\"\n", 29 | " return arg" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "class SomeClass(Enum):\n", 39 | " \"\"\"Test class for references.\"\"\"\n", 40 | "\n", 41 | " def __init__(self):\n", 42 | " self._field = 1\n", 43 | "\n", 44 | " def some_method1(self):\n", 45 | " \"\"\"Test method for references.\"\"\"\n", 46 | " return SOME_CONSTANT + self._field\n", 47 | "\n", 48 | " def some_method2(self):\n", 49 | " \"\"\"Test method for references.\"\"\"\n", 50 | " return some_function(SOME_CONSTANT)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "instance = SomeClass()\n", 60 | "instance.some_method1()\n", 61 | "instance.some_method2()" 62 | ] 63 | } 64 | ], 65 | "metadata": { 66 | "kernelspec": { 67 | "display_name": "jedi-language-server", 68 | "language": "python", 69 | "name": "python3" 70 | }, 71 | "language_info": { 72 | "name": "python", 73 | "version": "3.11.9" 74 | } 75 | }, 76 | "nbformat": 4, 77 | "nbformat_minor": 2 78 | } 79 | -------------------------------------------------------------------------------- /tests/test_data/references/references_test1.py: -------------------------------------------------------------------------------- 1 | """Test file for references.""" 2 | 3 | from enum import Enum 4 | 5 | SOME_CONSTANT = "something" 6 | 7 | 8 | def some_function(arg: str) -> str: 9 | """Test function for references.""" 10 | return arg 11 | 12 | 13 | class SomeClass(Enum): 14 | """Test class for references.""" 15 | 16 | def __init__(self): 17 | self._field = 1 18 | 19 | def some_method1(self): 20 | """Test method for references.""" 21 | return SOME_CONSTANT + self._field 22 | 23 | def some_method2(self): 24 | """Test method for references.""" 25 | return some_function(SOME_CONSTANT) 26 | 27 | 28 | instance = SomeClass() 29 | instance.some_method1() 30 | instance.some_method2() 31 | -------------------------------------------------------------------------------- /tests/test_data/semantic_tokens/semantic_tokens_test1.py: -------------------------------------------------------------------------------- 1 | """Test file for semantic tokens import. 2 | 3 | isort:skip_file 4 | """ 5 | 6 | import re 7 | import sys, os.path as path 8 | -------------------------------------------------------------------------------- /tests/test_data/semantic_tokens/semantic_tokens_test2.py: -------------------------------------------------------------------------------- 1 | from os.path import exists 2 | from sys import argv as arguments 3 | -------------------------------------------------------------------------------- /tests/test_data/signature/signature_test1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "def some_function(arg1: str, arg2: int, arg3: list):\n", 10 | " \"\"\"This is a test function.\"\"\"" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "some_function(\"a\", 1, [])" 20 | ] 21 | } 22 | ], 23 | "metadata": { 24 | "language_info": { 25 | "name": "python" 26 | } 27 | }, 28 | "nbformat": 4, 29 | "nbformat_minor": 2 30 | } 31 | -------------------------------------------------------------------------------- /tests/test_data/signature/signature_test1.py: -------------------------------------------------------------------------------- 1 | """Test file for signature help tests.""" 2 | 3 | 4 | def some_function(arg1: str, arg2: int, arg3: list): 5 | """This is a test function.""" 6 | 7 | 8 | some_function("a", 1, []) 9 | -------------------------------------------------------------------------------- /tests/test_data/symbol/somemodule.py: -------------------------------------------------------------------------------- 1 | """Module for testing symbols.""" 2 | 3 | 4 | def do_something(): 5 | """Function for symbol tests.""" 6 | -------------------------------------------------------------------------------- /tests/test_data/symbol/somemodule2.py: -------------------------------------------------------------------------------- 1 | """Module for testing symbols.""" 2 | 3 | 4 | def do_something_else(): 5 | """Function for symbol tests.""" 6 | 7 | 8 | def do_workspace_thing(): 9 | """Function for workspace symbol test.""" 10 | -------------------------------------------------------------------------------- /tests/test_data/symbol/symbol_test1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from typing import Any\n", 10 | "\n", 11 | "from . import somemodule, somemodule2" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "SOME_CONSTANT = 1" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "def do_work():\n", 30 | " \"\"\"Function for symbols test.\"\"\"\n", 31 | " somemodule.do_something()\n", 32 | " somemodule2.do_something_else()" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "class SomeClass:\n", 42 | " \"\"\"Class for symbols test.\"\"\"\n", 43 | "\n", 44 | " def __init__(self, arg1: Any):\n", 45 | " self.somedata = arg1\n", 46 | "\n", 47 | " def do_something(self):\n", 48 | " \"\"\"Method for symbols test.\"\"\"\n", 49 | "\n", 50 | " def so_something_else(self):\n", 51 | " \"\"\"Method for symbols test.\"\"\"" 52 | ] 53 | } 54 | ], 55 | "metadata": { 56 | "language_info": { 57 | "name": "python" 58 | } 59 | }, 60 | "nbformat": 4, 61 | "nbformat_minor": 2 62 | } 63 | -------------------------------------------------------------------------------- /tests/test_data/symbol/symbol_test1.py: -------------------------------------------------------------------------------- 1 | """Test file for symbol tests.""" 2 | 3 | from typing import Any 4 | 5 | from . import somemodule, somemodule2 6 | 7 | SOME_CONSTANT = 1 8 | 9 | 10 | def do_work(): 11 | """Function for symbols test.""" 12 | somemodule.do_something() 13 | somemodule2.do_something_else() 14 | 15 | 16 | class SomeClass: 17 | """Class for symbols test.""" 18 | 19 | def __init__(self, arg1: Any): 20 | self.somedata = arg1 21 | 22 | def do_something(self): 23 | """Method for symbols test.""" 24 | 25 | def so_something_else(self): 26 | """Method for symbols test.""" 27 | -------------------------------------------------------------------------------- /tests/test_debounce.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | from collections import Counter 4 | 5 | from jedi_language_server.constants import MAX_CONCURRENT_DEBOUNCE_CALLS 6 | from jedi_language_server.jedi_utils import debounce 7 | 8 | 9 | def test_debounce() -> None: 10 | """Test that the debounce decorator delays and limits the number of concurrent calls.""" 11 | # Create a function that records call counts per URI. 12 | counter = Counter[str]() 13 | cond = threading.Condition() 14 | 15 | def f(uri: str) -> None: 16 | with cond: 17 | counter.update(uri) 18 | cond.notify_all() 19 | 20 | # Call the debounced function more than the max allowed concurrent calls. 21 | debounced = debounce(interval_s=1, keyed_by="uri")(f) 22 | for _ in range(3): 23 | debounced("0") 24 | for i in range(1, MAX_CONCURRENT_DEBOUNCE_CALLS + 10): 25 | debounced(str(i)) 26 | 27 | # Wait for at least the max allowed concurrent timers to complete. 28 | with cond: 29 | assert cond.wait_for( 30 | lambda: sum(counter.values()) >= MAX_CONCURRENT_DEBOUNCE_CALLS 31 | ) 32 | 33 | # Check the counter after 0.5 seconds to ensure that no additional timers 34 | # were started and have completed. 35 | time.sleep(0.5) 36 | assert sum(counter.values()) == MAX_CONCURRENT_DEBOUNCE_CALLS 37 | 38 | # For uri "0", only one timer should have been started despite 3 calls. 39 | assert counter["0"] == 1 40 | -------------------------------------------------------------------------------- /tests/test_initialization_options.py: -------------------------------------------------------------------------------- 1 | """Test parsing of the initialization options.""" 2 | 3 | import re 4 | 5 | from hamcrest import assert_that, is_ 6 | 7 | from jedi_language_server.initialization_options import ( 8 | InitializationOptions, 9 | initialization_options_converter, 10 | ) 11 | 12 | 13 | def test_initialization_options() -> None: 14 | """Test our adjustments to parsing of the initialization options.""" 15 | initialization_options = initialization_options_converter.structure( 16 | { 17 | "completion": { 18 | "resolveEagerly": True, 19 | "ignorePatterns": [r"foo", r"bar/.*"], 20 | }, 21 | "hover": { 22 | "disable": { 23 | "keyword": {"all": False}, 24 | "class": {"all": True}, 25 | "function": {"all": True}, 26 | }, 27 | }, 28 | "extra": "ignored", 29 | }, 30 | InitializationOptions, 31 | ) 32 | 33 | assert_that(initialization_options.completion.resolve_eagerly, is_(True)) 34 | assert_that( 35 | initialization_options.completion.ignore_patterns, 36 | is_( 37 | [ 38 | re.compile(r"foo"), 39 | re.compile(r"bar/.*"), 40 | ] 41 | ), 42 | ) 43 | assert_that(initialization_options.hover.disable.keyword_.all, is_(False)) 44 | assert_that(initialization_options.hover.disable.class_.all, is_(True)) 45 | assert_that(initialization_options.hover.disable.function_.all, is_(True)) 46 | --------------------------------------------------------------------------------