├── .devcontainer ├── Dockerfile ├── devcontainer.json └── tools.mk ├── .editorconfig ├── .git-blame-ignore-revs ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── json-extension.yml │ └── release.yml ├── .gitignore ├── .readthedocs.yaml ├── .vscode ├── extensions │ └── pygls-playground │ │ ├── .eslintrc.yml │ │ ├── .gitignore │ │ ├── .vscodeignore │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── src │ │ └── extension.ts │ │ └── tsconfig.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── HISTORY.md ├── Implementations.md ├── LICENSE.txt ├── Makefile ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── RELEASING.md ├── ThirdPartyNotices.txt ├── cliff.toml ├── commitlintrc.yaml ├── docs ├── Makefile ├── assets │ ├── hello-world-completion.png │ └── semantic-tokens-example.png ├── generate_token_visualisation.py ├── make.bat ├── requirements.txt └── source │ ├── changelog.rst │ ├── clients │ └── index.rst │ ├── conf.py │ ├── contributing │ ├── howto.rst │ └── howto │ │ └── run-pyodide-test-suite.rst │ ├── ext │ └── examples.py │ ├── history.rst │ ├── implementations.rst │ ├── index.rst │ ├── protocol │ ├── howto.rst │ └── howto │ │ ├── interpret-semantic-tokens.rst │ │ └── tokens │ │ ├── modifiers.html │ │ ├── positions.html │ │ └── types.html │ ├── pygls │ ├── howto.rst │ ├── howto │ │ ├── migrate-to-v1.rst │ │ └── migrate-to-v2.rst │ ├── reference.rst │ └── reference │ │ ├── clients.rst │ │ ├── io.rst │ │ ├── protocol.rst │ │ ├── servers.rst │ │ ├── types.rst │ │ ├── uris.rst │ │ └── workspace.rst │ └── servers │ ├── examples │ ├── code-actions.rst │ ├── code-lens.rst │ ├── colors.rst │ ├── formatting.rst │ ├── goto.rst │ ├── hover.rst │ ├── inlay-hints.rst │ ├── json-server.rst │ ├── links.rst │ ├── publish-diagnostics.rst │ ├── pull-diagnostics.rst │ ├── rename.rst │ ├── semantic-tokens.rst │ ├── symbols.rst │ └── threaded-handlers.rst │ ├── getting-started.rst │ ├── howto.rst │ ├── howto │ ├── handle-invalid-data.rst │ ├── run-a-server-in-pyodide.rst │ └── use-the-pygls-playground.rst │ ├── tutorial.rst │ ├── tutorial │ ├── 0-setup.rst │ ├── y-testing.rst │ └── z-next-steps.rst │ └── user-guide.rst ├── examples ├── hello-world │ ├── README.md │ └── main.py └── servers │ ├── README.md │ ├── async_shutdown.py │ ├── code_actions.py │ ├── code_lens.py │ ├── colors.py │ ├── commands.py │ ├── formatting.py │ ├── goto.py │ ├── hover.py │ ├── inlay_hints.py │ ├── json_server.py │ ├── links.py │ ├── publish_diagnostics.py │ ├── pull_diagnostics.py │ ├── register_during_initialize.py │ ├── rename.py │ ├── semantic_tokens.py │ ├── symbols.py │ ├── threaded_handlers.py │ └── workspace │ ├── Untitled-1.ipynb │ ├── code.txt │ ├── colors.txt │ ├── dates.txt │ ├── links.txt │ ├── sums.txt │ ├── table.txt │ └── test.json ├── poetry.lock ├── pygls ├── __init__.py ├── capabilities.py ├── cli.py ├── client.py ├── constants.py ├── exceptions.py ├── feature_manager.py ├── io_.py ├── lsp │ ├── __init__.py │ ├── _base_client.py │ ├── _base_server.py │ ├── _capabilities.py │ ├── client.py │ └── server.py ├── progress.py ├── protocol │ ├── __init__.py │ ├── json_rpc.py │ └── language_server.py ├── py.typed ├── server.py ├── uris.py └── workspace │ ├── __init__.py │ ├── position_codec.py │ ├── text_document.py │ └── workspace.py ├── pyproject.toml ├── scripts ├── check_generated_code_is_uptodate.py ├── generate_code.py └── generate_contributors_md.py └── tests ├── __init__.py ├── _init_server_stall_fix_hack.py ├── client.py ├── conftest.py ├── e2e ├── test_async_shutdown.py ├── test_code_action.py ├── test_code_lens.py ├── test_colors.py ├── test_commands.py ├── test_completion.py ├── test_declaration.py ├── test_definition.py ├── test_formatting.py ├── test_hover.py ├── test_implementation.py ├── test_inlay_hints.py ├── test_links.py ├── test_publish_diagnostics.py ├── test_pull_diagnostics.py ├── test_references.py ├── test_register_during_initialize.py ├── test_rename.py ├── test_semantic_tokens.py ├── test_symbols.py ├── test_threaded_handlers.py └── test_type_definition.py ├── ls_setup.py ├── lsp ├── __init__.py ├── test_call_hierarchy.py ├── test_document_highlight.py ├── test_errors.py ├── test_folding_range.py ├── test_linked_editing_range.py ├── test_moniker.py ├── test_progress.py ├── test_selection_range.py ├── test_signature_help.py └── test_type_hierarchy.py ├── pyodide ├── .gitignore ├── package-lock.json ├── package.json └── run_server.js ├── servers ├── invalid_json.py └── large_response.py ├── test_client.py ├── test_document.py ├── test_feature_manager.py ├── test_language_server.py ├── test_protocol.py ├── test_server_connection.py ├── test_types.py ├── test_uris.py └── test_workspace.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 2 | 3 | COPY tools.mk / 4 | 5 | RUN su vscode -c "make -f tools.mk tools" \ 6 | && rm tools.mk 7 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu 3 | { 4 | "name": "pygls", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "build": { 7 | "dockerfile": "Dockerfile" 8 | }, 9 | "containerEnv": { 10 | "TZ": "UTC" 11 | }, 12 | // Features to add to the dev container. More info: https://containers.dev/features. 13 | // "features": {}, 14 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 15 | // "forwardPorts": [], 16 | // Use 'postCreateCommand' to run commands after the container is created. 17 | // "postCreateCommand": "uname -a", 18 | // Configure tool-specific properties. 19 | "customizations": { 20 | "vscode": { 21 | "extensions": [ 22 | "charliermarsh.ruff", 23 | "ms-python.python", 24 | ] 25 | } 26 | 27 | } 28 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 29 | // "remoteUser": "root" 30 | } 31 | -------------------------------------------------------------------------------- /.devcontainer/tools.mk: -------------------------------------------------------------------------------- 1 | ARCH ?= $(shell arch) 2 | BIN ?= $(HOME)/.local/bin 3 | 4 | ifeq ($(strip $(ARCH)),) 5 | $(error Unable to determine platform architecture) 6 | endif 7 | 8 | NODE_VERSION := 20.19.2 9 | UV_VERSION := 0.7.9 10 | 11 | UV ?= $(shell command -v uv) 12 | UVX ?= $(shell command -v uvx) 13 | 14 | ifeq ($(strip $(UV)),) 15 | 16 | UV := $(BIN)/uv 17 | UVX := $(BIN)/uvx 18 | 19 | $(UV): 20 | curl -L --output /tmp/uv.tar.gz https://github.com/astral-sh/uv/releases/download/$(UV_VERSION)/uv-$(ARCH)-unknown-linux-gnu.tar.gz 21 | tar -xf /tmp/uv.tar.gz -C /tmp 22 | rm /tmp/uv.tar.gz 23 | 24 | test -d $(BIN) || mkdir -p $(BIN) 25 | 26 | mv /tmp/uv-$(ARCH)-unknown-linux-gnu/uv $@ 27 | mv /tmp/uv-$(ARCH)-unknown-linux-gnu/uvx $(UVX) 28 | 29 | $@ --version 30 | $(UVX) --version 31 | 32 | endif 33 | 34 | # The versions of Python we support 35 | PYXX_versions := 3.10 3.11 3.12 3.13 3.14 36 | 37 | # Our default Python version 38 | PY_VERSION := 3.13 39 | 40 | # This effectively defines a function `PYXX` that takes a Python version number 41 | # (e.g. 3.8) and expands it out into a common block of code that will ensure a 42 | # verison of that interpreter is available to be used. 43 | # 44 | # This is perhaps a bit more complicated than I'd like, but it should mean that 45 | # the project's makefiles are useful both inside and outside of a devcontainer. 46 | # 47 | # `PYXX` has the following behavior: 48 | # - If possible, it will reuse the user's existing version of Python 49 | # i.e. $(shell command -v pythonX.X) 50 | # 51 | # - The user may force a specific interpreter to be used by setting the 52 | # variable when running make e.g. PYXX=/path/to/pythonX.X make ... 53 | # 54 | # - Otherwise, `make` will use `$(UV)` to install the given version of 55 | # Python under `$(BIN)` 56 | # 57 | # See: https://www.gnu.org/software/make/manual/html_node/Eval-Function.html 58 | define PYXX = 59 | 60 | PY$(subst .,,$1) ?= $$(shell command -v python$1) 61 | 62 | ifeq ($$(strip $$(PY$(subst .,,$1))),) 63 | 64 | PY$(subst .,,$1) := $$(BIN)/python$1 65 | 66 | $$(PY$(subst .,,$1)): | $$(UV) 67 | $$(UV) python find $1 || $$(UV) python install $1 68 | ln -s $$$$($$(UV) python find $1) $$@ 69 | 70 | $$@ --version 71 | 72 | endif 73 | 74 | endef 75 | 76 | # Uncomment the following line to see what this expands into. 77 | #$(foreach version,$(PYXX_versions),$(info $(call PYXX,$(version)))) 78 | $(foreach version,$(PYXX_versions),$(eval $(call PYXX,$(version)))) 79 | 80 | POETRY ?= $(shell command -v poetry) 81 | 82 | ifeq ($(strip $(POETRY)),) 83 | 84 | POETRY := $(BIN)/poetry 85 | 86 | $(POETRY): | $(UV) 87 | $(UV) tool install poetry 88 | $@ --version 89 | 90 | endif 91 | 92 | 93 | PY_TOOLS := $(POETRY) 94 | 95 | # Set a default `python` command if there is not one already 96 | PY ?= $(shell command -v python) 97 | 98 | ifeq ($(strip $(PY)),) 99 | PY := $(BIN)/python 100 | 101 | $(PY): | $(UV) 102 | $(UV) python install $(PY_VERSION) 103 | ln -s $$($(UV) python find $(PY_VERSION)) $@ 104 | $@ --version 105 | endif 106 | 107 | # Node JS 108 | NPM ?= $(shell command -v npm) 109 | NPX ?= $(shell command -v npx) 110 | 111 | ifeq ($(strip $(NPM)),) 112 | 113 | NPM := $(BIN)/npm 114 | NPX := $(BIN)/npx 115 | NODE := $(BIN)/node 116 | NODE_DIR := $(HOME)/.local/node 117 | 118 | $(NPM): 119 | curl -L --output /tmp/node.tar.xz https://nodejs.org/dist/v$(NODE_VERSION)/node-v$(NODE_VERSION)-linux-x64.tar.xz 120 | tar -xJf /tmp/node.tar.xz -C /tmp 121 | rm /tmp/node.tar.xz 122 | 123 | [ -d $(NODE_DIR) ] || mkdir -p $(NODE_DIR) 124 | mv /tmp/node-v$(NODE_VERSION)-linux-x64/* $(NODE_DIR) 125 | 126 | [ -d $(BIN) ] || mkdir -p $(BIN) 127 | ln -s $(NODE_DIR)/bin/node $(NODE) 128 | ln -s $(NODE_DIR)/bin/npm $(NPM) 129 | ln -s $(NODE_DIR)/bin/npx $(NPX) 130 | 131 | $(NODE) --version 132 | PATH=$(BIN) $(NPM) --version 133 | PATH=$(BIN) $(NPX) --version 134 | 135 | endif 136 | 137 | # One command to bootstrap all tools and check their versions 138 | .PHONY: tools 139 | tools: $(UV) $(PY) $(PY_TOOLS) $(NPM) $(NPX) 140 | for prog in $^ ; do echo -n "$${prog}\t" ; PATH=$(BIN) $${prog} --version; done 141 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = true 8 | end_of_line = lf 9 | 10 | [*.{yml,yaml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This file contains a list of commits that are not likely what you 2 | # are looking for in a blame, such as mass reformatting or renaming. 3 | # You can set this file as a default ignore file for blame by running 4 | # the following command. 5 | # 6 | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs 7 | # 8 | # To temporarily not use this file add 9 | # --ignore-revs-file="" 10 | # to your blame command. 11 | # 12 | # The ignoreRevsFile can't be set globally due to blame failing if the file isn't present. 13 | # To not have to set the option in every repository it is needed in, 14 | # save the following script in your path with the name "git-bblame" 15 | # now you can run 16 | # $ git bblame $FILE 17 | # to use the .git-blame-ignore-revs file if it is present. 18 | # 19 | # #!/usr/bin/env bash 20 | # repo_root=$(git rev-parse --show-toplevel) 21 | # if [[ -e $repo_root/.git-blame-ignore-revs ]]; then 22 | # git blame --ignore-revs-file="$repo_root/.git-blame-ignore-revs" $@ 23 | # else 24 | # git blame $@ 25 | # fi 26 | 27 | 28 | # chore: introduce `black` formatting 29 | 86b36e271ebde5ac4f30bf83c4e7ee42ba5af9ac 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # These settings are for any web project 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # Force the following filetypes to have unix eols, so Windows does not break them 8 | *.* text eol=lf 9 | 10 | # 11 | ## These files are binary and should be left untouched 12 | # 13 | 14 | # (binary is a macro for -text -diff) 15 | *.png binary 16 | *.jpg binary 17 | *.jpeg binary 18 | *.gif binary 19 | *.ico binary 20 | *.mov binary 21 | *.mp4 binary 22 | *.mp3 binary 23 | *.flv binary 24 | *.fla binary 25 | *.swf binary 26 | *.gz binary 27 | *.zip binary 28 | *.7z binary 29 | *.ttf binary 30 | *.eot binary 31 | *.woff binary 32 | *.pyc binary 33 | *.pdf binary 34 | *.ez binary 35 | *.bz2 binary 36 | *.swp binary 37 | *.whl binary 38 | *.docx binary 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: OpenLawLibrary 4 | -------------------------------------------------------------------------------- /.github/workflows/json-extension.yml: -------------------------------------------------------------------------------- 1 | name: pygls-playground 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | defaults: 17 | run: 18 | shell: bash 19 | working-directory: .vscode/extensions/pygls-playground 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-python@v5 25 | with: 26 | python-version: "3.x" 27 | 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: "18.x" 31 | cache: 'npm' 32 | cache-dependency-path: '.vscode/extensions/pygls-playground/package-lock.json' 33 | 34 | - name: Install dependencies 35 | run: | 36 | npm ci 37 | npm i vsce 38 | 39 | - name: Lint 40 | run: npx eslint src/*.ts 41 | 42 | - name: Compile 43 | run: npm run compile 44 | 45 | - name: Replace package.json version 46 | run: | 47 | replace_packagejson_version() { 48 | version_line=$(grep -o '"version".*' $1) 49 | version=$(python -m json.tool package.json | awk -F'"' '/version/{print $4}') 50 | build_version=$version+$2 51 | build_version_line=${version_line/$version/$build_version} 52 | sed -i "s|$version_line|$build_version_line|g" $1 53 | 54 | cat $1 55 | } 56 | 57 | replace_packagejson_version package.json $GITHUB_RUN_ID 58 | 59 | - name: Build VSIX 60 | run: npx vsce package 61 | 62 | - name: Validate VSIX 63 | run: | 64 | npx vsce ls | grep package.json 65 | npx vsce ls | grep out/extension.js 66 | 67 | - name: Upload VSIX 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: pygls-playground-vsix 71 | # The path must be rooted from the directory GitHub Actions starts 72 | # from, not the working-directory. 73 | path: .vscode/extensions/pygls-playground/*.vsix 74 | if-no-files-found: error 75 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Pygls to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | relase: 9 | name: "🚀 Release 🚢" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | ssh-key: ${{secrets.CI_RELEASE_DEPLOY_KEY}} 16 | fetch-depth: 0 17 | - name: Use Python "3.10" 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: "3.10" 21 | - name: Install Poetry 22 | uses: snok/install-poetry@v1 23 | - name: Generate the latest changelog 24 | uses: orhun/git-cliff-action@v2 25 | id: git-cliff 26 | with: 27 | config: cliff.toml 28 | args: --verbose --latest 29 | env: 30 | OUTPUT: git-cliff-changes.tmp.md 31 | - name: Update the changelog 32 | run: | 33 | git checkout main 34 | cat git-cliff-changes.tmp.md | sed -i "3r /dev/stdin" CHANGELOG.md 35 | git config --global user.name 'Github Action' 36 | git config --global user.email 'github.action@users.noreply.github.com' 37 | git add CHANGELOG.md 38 | git commit -m "chore: update CHANGELOG.md" 39 | git push 40 | - name: Update CONTRIBUTORS.md 41 | run: | 42 | git checkout main 43 | poetry install 44 | poetry run poe generate_contributors_md 45 | if [[ $(git diff --stat CONTRIBUTORS.md) != '' ]]; then 46 | git add CONTRIBUTORS.md 47 | git commit -m "chore: update CONTRIBUTORS.md" 48 | git push 49 | fi 50 | - name: Release 51 | run: | 52 | poetry build 53 | poetry publish --username "__token__" --password ${{ secrets.PYPI_API_TOKEN }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .dir-locals.el 3 | 4 | *.vsix 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | cov.pth 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # OS files 110 | .DS_Store 111 | 112 | # mypy 113 | .mypy_cache 114 | .dmypy.json 115 | 116 | /pyodide_testrunner 117 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the "docs/" directory with Sphinx 15 | sphinx: 16 | configuration: docs/source/conf.py 17 | 18 | # Optional but recommended, declare the Python requirements required 19 | # to build your documentation 20 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 21 | python: 22 | install: 23 | - requirements: docs/requirements.txt 24 | - method: pip 25 | path: . 26 | -------------------------------------------------------------------------------- /.vscode/extensions/pygls-playground/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es2021: true 3 | node: true 4 | extends: 5 | - 'eslint:recommended' 6 | - 'plugin:@typescript-eslint/recommended' 7 | parser: '@typescript-eslint/parser' 8 | parserOptions: 9 | ecmaVersion: 12 10 | sourceType: module 11 | plugins: 12 | - '@typescript-eslint' 13 | rules: {} 14 | -------------------------------------------------------------------------------- /.vscode/extensions/pygls-playground/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | client/server 4 | .vscode-test 5 | .vscode/settings.json 6 | env 7 | -------------------------------------------------------------------------------- /.vscode/extensions/pygls-playground/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .gitignore 3 | client/out/*.map 4 | client/src/ 5 | tsconfig.json 6 | tslint.json 7 | package.json 8 | package-lock.json 9 | 10 | .pytest_cache 11 | -------------------------------------------------------------------------------- /.vscode/extensions/pygls-playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "lib": [ 6 | "ES2019" 7 | ], 8 | "rootDir": "src", 9 | "outDir": "out", 10 | "sourceMap": true 11 | }, 12 | "include": [ 13 | "src" 14 | ], 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "pygls: Debug Server", 6 | "type": "debugpy", 7 | "request": "attach", 8 | "connect": { 9 | "host": "${config:pygls.server.debugHost}", 10 | "port": "${config:pygls.server.debugPort}" 11 | }, 12 | "justMyCode": false 13 | }, 14 | { 15 | "name": "pygls: Debug Client", 16 | "type": "extensionHost", 17 | "request": "launch", 18 | "runtimeExecutable": "${execPath}", 19 | "args": [ 20 | "--extensionDevelopmentPath=${workspaceRoot}/.vscode/extensions/pygls-playground", 21 | "--folder-uri=${workspaceRoot}/examples/servers", 22 | ], 23 | "outFiles": [ 24 | "${workspaceRoot}/.vscode/extensions/pygls-playground/out/**/*.js" 25 | ], 26 | "preLaunchTask": { 27 | "type": "npm", 28 | "script": "watch" 29 | }, 30 | }, 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[plaintext]": { 3 | // Uncomment to enable `textDocument/onTypeFormatting` requests 4 | // "editor.formatOnType": true 5 | }, 6 | // Uncomment to override Python interpreter used. 7 | // "pygls.server.pythonPath": "/path/to/python", 8 | "pygls.server.debug": false, 9 | // "pygls.server.debugHost": "localhost", 10 | // "pygls.server.debugPort": 5678, 11 | "pygls.server.launchScript": "code_actions.py", // This is relative to `pygls.server.cwd` 12 | "pygls.server.cwd": "${workspaceFolder}/examples/servers", 13 | "pygls.trace.server": "off", 14 | "pygls.client.documentSelector": [ 15 | { 16 | "scheme": "file", 17 | "language": "plaintext" 18 | } 19 | ], 20 | "python.testing.pytestArgs": [ 21 | "." 22 | ], 23 | "python.testing.unittestEnabled": false, 24 | "python.testing.pytestEnabled": true, 25 | // "pygls.jsonServer.exampleConfiguration": "some value here", 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "compile", 7 | "group": "build", 8 | "presentation": { 9 | "panel": "dedicated", 10 | "reveal": "never" 11 | }, 12 | "problemMatcher": ["$tsc"] 13 | }, 14 | { 15 | "type": "npm", 16 | "script": "watch", 17 | "isBackground": true, 18 | "group": { 19 | "kind": "build", 20 | "isDefault": true 21 | }, 22 | "presentation": { 23 | "panel": "dedicated", 24 | "reveal": "never" 25 | }, 26 | "problemMatcher": ["$tsc-watch"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others’ private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to _pygls_ 2 | 3 | Welcome, and thank you for your interest in contributing to _pygls_! 4 | 5 | There are many ways in which you can contribute, beyond writing code. 6 | 7 | ## Reporting Issues 8 | 9 | Have you identified a reproducible problem in _pygls_? Have a feature request? We want to hear about it! Here's how you can make reporting your issue as effective as possible. 10 | 11 | ### Look For an Existing Issue 12 | 13 | Before you create a new issue, please do a search in [open issues](https://github.com/openlawlibrary/pygls/issues) to see if the issue or feature request has already been filed. 14 | 15 | Be sure to scan through the [most popular](https://github.com/openlawlibrary/pygls/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) feature requests. 16 | 17 | If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment: 18 | 19 | * 👍 - upvote 20 | * 👎 - downvote 21 | 22 | If you cannot find an existing issue that describes your bug or feature, create a new issue using the guidelines below. 23 | 24 | ### Writing Good Bug Reports and Feature Requests 25 | 26 | File a single issue per problem and feature request. Do not enumerate multiple bugs or feature requests in the same issue. 27 | 28 | Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes. 29 | 30 | The more information you can provide, the more likely someone will be successful at reproducing the issue and finding a fix. 31 | Please include the following with each issue: 32 | 33 | * Reproducible steps (1... 2... 3...) that cause the issue 34 | * What you expected to see, versus what you actually saw 35 | * Images, animations, or a link to a video showing the issue occurring, if appropriate 36 | * A code snippet that demonstrates the issue or a link to a code repository the developers can easily pull down to recreate the issue locally 37 | * **Note:** Because the developers need to copy and paste the code snippet, including a code snippet as a media file (i.e. .gif) is not sufficient. 38 | * If using VS Code, errors from the Dev Tools Console (open from the menu: Help > Toggle Developer Tools) 39 | * We follow [Convention Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages. For example, a commit should have a category and use lower case: `feat: solved the halthing problem`. 40 | 41 | ### Final Checklist 42 | 43 | Please remember to do the following: 44 | 45 | * [ ] Search the issue repository to ensure your report is a new issue 46 | * [ ] Simplify your code around the issue to better isolate the problem 47 | 48 | Don't feel bad if the developers can't reproduce the issue right away. They will simply ask for more information! 49 | 50 | ## Thank You 51 | 52 | Your contributions to open source, large or small, make great projects like this possible. Thank you for taking the time to contribute. 53 | 54 | ## Attribution 55 | 56 | This _Contributing to pygls_ document is adapted from VS Code's _[Contributing to VS Code](https://github.com/Microsoft/vscode/blob/master/CONTRIBUTING.md)_. 57 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors (contributions) 2 | * [alcarney](https://github.com/alcarney) (223) 3 | * [augb](https://github.com/augb) (35) 4 | * [bollwyvl](https://github.com/bollwyvl) (1) 5 | * [brettcannon](https://github.com/brettcannon) (3) 6 | * [danixeee](https://github.com/danixeee) (321) 7 | * [deathaxe](https://github.com/deathaxe) (24) 8 | * [dependabot[bot]](https://github.com/apps/dependabot) (18) 9 | * [dgreisen](https://github.com/dgreisen) (4) 10 | * [dimbleby](https://github.com/dimbleby) (14) 11 | * [dinvlad](https://github.com/dinvlad) (9) 12 | * [HankBO](https://github.com/HankBO) (2) 13 | * [karthiknadig](https://github.com/karthiknadig) (13) 14 | * [KOLANICH](https://github.com/KOLANICH) (3) 15 | * [LaurenceWarne](https://github.com/LaurenceWarne) (2) 16 | * [MatejKastak](https://github.com/MatejKastak) (3) 17 | * [Maxattax97](https://github.com/Maxattax97) (1) 18 | * [muffinmad](https://github.com/muffinmad) (2) 19 | * [noklam](https://github.com/noklam) (3) 20 | * [nthykier](https://github.com/nthykier) (1) 21 | * [oliversen](https://github.com/oliversen) (2) 22 | * [pappasam](https://github.com/pappasam) (7) 23 | * [perrinjerome](https://github.com/perrinjerome) (25) 24 | * [psacawa](https://github.com/psacawa) (1) 25 | * [pschanely](https://github.com/pschanely) (1) 26 | * [renatav](https://github.com/renatav) (2) 27 | * [RossBencina](https://github.com/RossBencina) (5) 28 | * [tombh](https://github.com/tombh) (73) 29 | * [tsugumi-sys](https://github.com/tsugumi-sys) (1) 30 | * [Viicos](https://github.com/Viicos) (2) 31 | * [zanieb](https://github.com/zanieb) (5) 32 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History of [_pygls_][pygls] 2 | 3 | This is the story of [_pygls_][pygls]' inception as recounted by its original project creator, [@augb][augb]. 4 | 5 | While working at [Open Law Library][openlaw] as a programmer, we created a VS Code extension originally written in TypeScript called _Codify_. _Codify_ processes legal XML into legal code. Since our codification process was written in Python we were faced with the choice of slower performance to roundtrip from TypeScript to Python and back, or duplicating the logic in TypeScript. Neither option was really good. I had the idea of using the [Language Server Protocol (LSP)][lsp] to communicate with a Python LSP server. Existing Python language servers were focused on Python the language. We needed a generic language server since we were dealing with XML. [David Greisen][dgreisen], agreed with this approach. Thus, [_pygls_][pygls] was born. 6 | 7 | I, [@augb][augb], was the project manager for the project. Daniel Elero ([@danixeee][danixeee]) did the coding. When I left Open Law Library, Daniel took over the project for a time. 8 | 9 | It was open sourced on December 21, 2018. The announcement on Hacker News is [here][announcement]. 10 | 11 | [augb]: https://github.com/augb 12 | [announcement]: https://news.ycombinator.com/item?id=18735413 13 | [danixeee]: https://github.com/danixeee 14 | [dgreisen]: https://github.com/dgreisen 15 | [lsp]: https://microsoft.github.io/language-server-protocol/specification 16 | [openlaw]: https://openlawlib.org/ 17 | [pygls]: https://github.com/openlawlibrary/pygls 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: dist 2 | dist: | $(POETRY) 3 | $(POETRY) install --all-extras 4 | git describe --tags --abbrev=0 5 | $(POETRY) build 6 | 7 | .PHONY: lint 8 | lint: | $(POETRY) 9 | $(POETRY) install --all-extras --with dev 10 | $(POETRY) run poe lint 11 | 12 | .PHONY: test 13 | test: | $(POETRY) 14 | $(POETRY) install --all-extras 15 | $(POETRY) run poe test 16 | 17 | .PHONY: test-pyodide 18 | test-pyodide: dist | $(NPM) $(POETRY) 19 | $(POETRY) install --with test 20 | cd tests/pyodide && $(NPM) ci 21 | $(POETRY) run poe test-pyodide 22 | 23 | .PHONY: pygls-playground 24 | pygls-playground: | $(NPM) $(POETRY) 25 | $(POETRY) install --all-extras 26 | cd .vscode/extensions/pygls-playground && $(NPM) install --no-save 27 | cd .vscode/extensions/pygls-playground && $(NPM) run compile 28 | 29 | include .devcontainer/tools.mk 30 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description (e.g. "Related to ...", etc.) 2 | 3 | _Please replace this description with a concise description of this Pull Request._ 4 | 5 | ## Code review checklist (for code reviewer to complete) 6 | 7 | - [ ] Pull request represents a single change (i.e. not fixing disparate/unrelated things in a single PR) 8 | - [ ] Title summarizes what is changing 9 | - [ ] Commit messages are meaningful (see [this][commit messages] for details) 10 | - [ ] Tests have been included and/or updated, as appropriate 11 | - [ ] Docstrings have been included and/or updated, as appropriate 12 | - [ ] Standalone docs have been updated accordingly 13 | 14 | ## Automated linters 15 | 16 | You can run the lints that are run on CI locally with: 17 | ```sh 18 | poetry install --all-extras --with dev 19 | poetry run poe lint 20 | ``` 21 | 22 | [commit messages]: https://conventionalcommits.org/ 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI Version](https://img.shields.io/pypi/v/pygls.svg)](https://pypi.org/project/pygls/) ![!pyversions](https://img.shields.io/pypi/pyversions/pygls.svg) ![license](https://img.shields.io/pypi/l/pygls.svg) [![Documentation Status](https://img.shields.io/badge/docs-latest-green.svg)](https://pygls.readthedocs.io/en/latest/) 2 | 3 | # pygls: The Generic Language Server Framework 4 | 5 | _pygls_ (pronounced like "pie glass") is a pythonic generic implementation of the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/specification) for use as a foundation for writing your own [Language Servers](https://langserver.org/) in just a few lines of code. 6 | 7 | > [!IMPORTANT] 8 | > The next major version, *pygls v2* is ready for testing. 9 | > We encourage all existing server authors to try the [migration guide](https://pygls.readthedocs.io/en/latest/pygls/howto/migrate-to-v2.html) and let us know how you get on! 10 | 11 | ## Quickstart 12 | ```python 13 | from pygls.lsp.server import LanguageServer 14 | from lsprotocol import types 15 | 16 | server = LanguageServer("example-server", "v0.1") 17 | 18 | @server.feature(types.TEXT_DOCUMENT_COMPLETION) 19 | def completions(params: types.CompletionParams): 20 | items = [] 21 | document = server.workspace.get_text_document(params.text_document.uri) 22 | current_line = document.lines[params.position.line].strip() 23 | if current_line.endswith("hello."): 24 | items = [ 25 | types.CompletionItem(label="world"), 26 | types.CompletionItem(label="friend"), 27 | ] 28 | return types.CompletionList(is_incomplete=False, items=items) 29 | 30 | server.start_io() 31 | ``` 32 | 33 | Which might look something like this when you trigger autocompletion in your editor: 34 | 35 | ![completions](https://raw.githubusercontent.com/openlawlibrary/pygls/master/docs/assets/hello-world-completion.png) 36 | 37 | ## Docs and Tutorial 38 | 39 | The full documentation and a tutorial are available at . 40 | 41 | ## Projects based on _pygls_ 42 | 43 | We keep a table of all known _pygls_ [implementations](https://github.com/openlawlibrary/pygls/blob/master/Implementations.md). Please submit a Pull Request with your own or any that you find are missing. 44 | 45 | ## Alternatives 46 | 47 | The main alternative to _pygls_ is Microsoft's [NodeJS-based Generic Language Server Framework](https://github.com/microsoft/vscode-languageserver-node). Being from Microsoft it is focussed on extending VSCode, although in theory it could be used to support any editor. So this is where pygls might be a better choice if you want to support more editors, as pygls is not focussed around VSCode. 48 | 49 | There are also other Language Servers with "general" in their descriptons, or at least intentions. They are however only general in the sense of having powerful _configuration_. They achieve generality in so much as configuration is able to, as opposed to what programming (in _pygls'_ case) can achieve. 50 | * https://github.com/iamcco/diagnostic-languageserver 51 | * https://github.com/mattn/efm-langserver 52 | * https://github.com/jose-elias-alvarez/null-ls.nvim (Neovim only) 53 | 54 | ## Tests 55 | All Pygls sub-tasks require the Poetry `poe` plugin: https://github.com/nat-n/poethepoet 56 | 57 | * `poetry install --all-extras` 58 | * `poetry run poe test` 59 | * `poetry run poe test-pyodide` 60 | 61 | 62 | ## Contributing 63 | 64 | Your contributions to _pygls_ are most welcome ❤️ Please review the [Contributing](https://github.com/openlawlibrary/pygls/blob/master/CONTRIBUTING.md) and [Code of Conduct](https://github.com/openlawlibrary/pygls/blob/master/CODE_OF_CONDUCT.md) documents for how to get started. 65 | 66 | ## Donating 67 | 68 | [Open Law Library](http://www.openlawlib.org/) is a 501(c)(3) tax exempt organization. Help us maintain our open source projects and open the law to all with [sponsorship](https://github.com/sponsors/openlawlibrary). 69 | 70 | ### Supporters 71 | 72 | We would like to give special thanks to the following supporters: 73 | * [mpourmpoulis](https://github.com/mpourmpoulis) 74 | 75 | ## License 76 | 77 | Apache-2.0 78 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # How To Make A New Release Of PyGLS 2 | 3 | 1. Update the version number in `pyproject.toml` 4 | 5 | - Try to follow https://semver.org/ 6 | - Commit message title should be something like: `build: v1.3.0` 7 | - Typically you'll want to make a dedicated PR for the version bump. But if a previous PR has already merged the wanted version bump into main, then a new PR is not necessary. 8 | - Example PR for a release https://github.com/openlawlibrary/pygls/pull/434 9 | - Merge the PR 10 | 11 | 2. Create a Github Release 12 | 13 | - Goto https://github.com/openlawlibrary/pygls/releases/new 14 | - In the "Choose a tag" dropdown button, type the new version number. 15 | - Click the "Generate release notes" button. 16 | - Generating release notes does not require making an actual release, so you may like to copy and paste the output of the release notes to make the above release PR more informative. 17 | - If it's a pre-release, remember to check the "Set as a pre-release" toggle. 18 | - Click the green "Publish release button". 19 | - Once the Github Release Action is complete (see https://github.com/openlawlibrary/pygls/actions), check https://pypi.org/project/pygls/ to verify that the new release is available. 20 | 21 | ## Notes 22 | 23 | - `CHANGELOG.md` and `CONTRIBUTORS.md` are automatically populated in the Github Release Action. 24 | - PyPi automatically detects beta and alpha versions from the version string, eg `v1.0.0a`, and prevents them from being the latest version. In other words, they're made publicly available but downstream projects with loose pinning (eg `^1.0.0`) won't automatically install them. 25 | -------------------------------------------------------------------------------- /ThirdPartyNotices.txt: -------------------------------------------------------------------------------- 1 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 2 | For Open Law Library pygls 3 | 4 | This project incorporates material from the project(s) listed below (collectively, “Third Party Code”). Open Law Library is not the original author of the Third Party Code. The original copyright notice and license under which Open Law Library received such Third Party Code are set out below. This Third Party Code is licensed to you under their original license terms set forth below. Open Law Library reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. 5 | 6 | 7 | 1) python-language-server 8 | (https://github.com/palantir/python-language-server) 9 | 10 | Copyright 2017 Palantir Technologies, Inc. 11 | 12 | MIT License 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | 32 | 2) vscode-extension-samples 33 | (https://github.com/Microsoft/vscode-extension-samples) 34 | 35 | Copyright (c) Microsoft Corporation 36 | 37 | All rights reserved. 38 | 39 | MIT License 40 | 41 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 42 | files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, 43 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 44 | is furnished to do so, subject to the following conditions: 45 | 46 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 47 | 48 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 49 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 50 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 51 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 52 | 53 | 3) All third party packages in node_modules folders are licensed under the licenses specified in those packages. 54 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | [changelog] 2 | header = "" 3 | # template for the changelog body 4 | # https://tera.netlify.app/docs 5 | body = """ 6 | {% if version %}\ 7 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 8 | {% else %}\ 9 | ## [unreleased] 10 | {% endif %}\ 11 | More details: https://github.com/openlawlibrary/pygls/releases/tag/{{version}} 12 | {% for group, commits in commits | group_by(attribute="group") %} 13 | ### {{ group | upper_first }} 14 | {% for commit in commits %} 15 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 16 | {% endfor %} 17 | {% endfor %}\n 18 | """ 19 | # remove the leading and trailing whitespace from the template 20 | trim = true 21 | # changelog footer 22 | footer = "" 23 | 24 | [git] 25 | # parse the commits based on https://www.conventionalcommits.org 26 | conventional_commits = true 27 | # filter out the commits that are not conventional 28 | filter_unconventional = false # TODO: Toggle after v1.0.3 as it introduces commit linting 29 | # process each line of a commit as an individual commit 30 | split_commits = false 31 | # regex for preprocessing the commit messages 32 | commit_preprocessors = [ 33 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/openlawlibrary/pygls/issues/${2}))"}, # replace issue numbers 34 | ] 35 | # regex for parsing and grouping commits 36 | commit_parsers = [ 37 | { message = "^feat", group = "Features" }, 38 | { message = "^fix", group = "Bug Fixes" }, 39 | { message = "^doc", group = "Documentation" }, 40 | { message = "^perf", group = "Performance" }, 41 | { message = "^refactor", group = "Refactor" }, 42 | { message = "^style", group = "Styling" }, 43 | { message = "^test", group = "Testing" }, 44 | { message = "^ci", group = "CI" }, 45 | { message = "^chore\\(release\\): prepare for", skip = true }, 46 | { message = "^chore", group = "Miscellaneous Tasks" }, 47 | { body = ".*security", group = "Security" }, 48 | ] 49 | # protect breaking changes from being skipped due to matching a skipping commit_parser 50 | protect_breaking_commits = false 51 | # filter out the commits that are not matched by commit parsers 52 | filter_commits = false 53 | # glob pattern for matching git tags 54 | tag_pattern = "v[0-9]*" 55 | # regex for skipping tags 56 | skip_tags = "v0.1.0-beta.1" 57 | # regex for ignoring tags 58 | ignore_tags = "" 59 | # sort the tags topologically 60 | topo_order = false 61 | # sort the commits inside sections by oldest/newest order 62 | sort_commits = "oldest" 63 | # limit the number of commits included in the changelog. 64 | # limit_commits = 42 65 | -------------------------------------------------------------------------------- /commitlintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # The rules below have been manually copied from @commitlint/config-conventional 3 | # and match the v1.0.0 specification: 4 | # https://www.conventionalcommits.org/en/v1.0.0/#specification 5 | # 6 | # You can remove them and uncomment the config below when the following issue is 7 | # fixed: https://github.com/conventional-changelog/commitlint/issues/613 8 | # 9 | # extends: 10 | # - '@commitlint/config-conventional' 11 | rules: 12 | body-leading-blank: [1, always] 13 | body-max-line-length: [2, always, Infinity] 14 | footer-leading-blank: [1, always] 15 | footer-max-line-length: [2, always, 100] 16 | header-max-length: [2, always, 100] 17 | subject-case: 18 | - 2 19 | - never 20 | - [sentence-case, start-case, pascal-case, upper-case] 21 | subject-empty: [2, never] 22 | subject-full-stop: [2, never, "."] 23 | type-case: [2, always, lower-case] 24 | type-empty: [2, never] 25 | type-enum: 26 | - 2 27 | - always 28 | - [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] 29 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/assets/hello-world-completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlawlibrary/pygls/34f646f8441ab9fab825ce61ab3f25771d15a88f/docs/assets/hello-world-completion.png -------------------------------------------------------------------------------- /docs/assets/semantic-tokens-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlawlibrary/pygls/34f646f8441ab9fab825ce61ab3f25771d15a88f/docs/assets/semantic-tokens-example.png -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGELOG.md 2 | :parser: myst_parser.sphinx_ 3 | -------------------------------------------------------------------------------- /docs/source/clients/index.rst: -------------------------------------------------------------------------------- 1 | Coming Soon\ :sup:`TM` 2 | ====================== 3 | -------------------------------------------------------------------------------- /docs/source/contributing/howto.rst: -------------------------------------------------------------------------------- 1 | How To 2 | ====== 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | Run the Pyodide Tests 8 | -------------------------------------------------------------------------------- /docs/source/ext/examples.py: -------------------------------------------------------------------------------- 1 | """Documentation for the example servers""" 2 | 3 | from __future__ import annotations 4 | 5 | import importlib.util as imutil 6 | import os 7 | import pathlib 8 | import typing 9 | 10 | from docutils.parsers.rst import directives 11 | from sphinx.util.docutils import SphinxDirective 12 | from sphinx.util.logging import getLogger 13 | 14 | if typing.TYPE_CHECKING: 15 | from sphinx.application import Sphinx 16 | 17 | logger = getLogger(__name__) 18 | 19 | 20 | class ExampleServerDirective(SphinxDirective): 21 | """Automate the process of documenting example servers. 22 | 23 | Currently, this doesn't do *that* much, it 24 | 25 | - Inserts the code using a ``.. literalinclude::`` directive. 26 | - Extracts the server module's docstring and inserts it into the page as nicely 27 | rendered text. 28 | 29 | But perhaps we can do something more interesting in the future! 30 | """ 31 | 32 | required_arguments = 1 33 | option_spec = { 34 | "start-at": directives.unchanged, 35 | } 36 | 37 | def get_docstring(self, filename: pathlib.Path): 38 | """Given the filepath to a module, return its docstring.""" 39 | 40 | base = filename.stem 41 | spec = imutil.spec_from_file_location(f"examples.{base}", filename) 42 | 43 | try: 44 | module = imutil.module_from_spec(spec) 45 | spec.loader.exec_module(module) 46 | except Exception: 47 | logger.exception("Unable to import example server") 48 | return [] 49 | 50 | if (docstring := module.__doc__) is not None: 51 | return docstring.splitlines() 52 | 53 | return [] 54 | 55 | def run(self): 56 | server_dir = self.config.example_server_dir 57 | name = self.arguments[0] 58 | 59 | if not (filename := pathlib.Path(server_dir, name)).exists(): 60 | raise RuntimeError(f"Unable to find example server: {filename}") 61 | 62 | # Tell Sphinx to rebuild a document if this file changes 63 | self.env.note_dependency(str(filename)) 64 | 65 | # An "absolute" path given to `literalinclude` is actually relative to the 66 | # projects srcdir 67 | relpath = os.path.relpath(str(filename), start=str(self.env.app.srcdir)) 68 | content = [ 69 | f".. literalinclude:: /{relpath}", 70 | " :language: python", 71 | ] 72 | 73 | if (start_at := self.options.get("start-at")) is not None: 74 | content.append(f" :start-at: {start_at}") 75 | 76 | # Confusingly, these are processed in reverse order... 77 | self.state_machine.insert_input(content, "") 78 | self.state_machine.insert_input(self.get_docstring(filename), str(filename)) 79 | 80 | return [] 81 | 82 | 83 | def setup(app: Sphinx): 84 | app.add_config_value("example_server_dir", "", rebuild="env") 85 | app.add_directive("example-server", ExampleServerDirective) 86 | -------------------------------------------------------------------------------- /docs/source/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../HISTORY.md 2 | :parser: myst_parser.sphinx_ 3 | -------------------------------------------------------------------------------- /docs/source/implementations.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../Implementations.md 2 | :parser: myst_parser.sphinx_ 3 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | *pygls* 2 | ======= 3 | 4 | `pygls`_ (pronounced like “pie glass”) is a generic implementation of 5 | the `Language Server Protocol`_ written in the Python programming language. It 6 | allows you to write your own `language server`_ in just a few lines of code 7 | 8 | .. literalinclude:: ../../examples/hello-world/main.py 9 | :language: python 10 | 11 | *pygls* supports 12 | 13 | - Python 3.9+ on Windows, MacOS and Linux 14 | - **Experimental** support for Pyodide 15 | - STDIO, TCP/IP and WEBSOCKET communication 16 | - Both sync and async styles of programming 17 | - Running code in background threads 18 | - Automatic text and notebook document syncronisation 19 | 20 | .. toctree:: 21 | :hidden: 22 | :caption: Language Servers 23 | 24 | servers/getting-started 25 | servers/user-guide 26 | How To 27 | 28 | .. toctree:: 29 | :hidden: 30 | :caption: Language Clients 31 | 32 | clients/index 33 | 34 | .. toctree:: 35 | :hidden: 36 | :caption: The Protocol 37 | 38 | protocol/howto 39 | 40 | .. toctree:: 41 | :hidden: 42 | :caption: The Library 43 | 44 | pygls/howto 45 | pygls/reference 46 | 47 | .. toctree:: 48 | :hidden: 49 | :caption: Contributing 50 | 51 | contributing/howto 52 | 53 | .. toctree:: 54 | :hidden: 55 | :caption: About 56 | 57 | implementations 58 | history 59 | changelog 60 | 61 | Navigation 62 | ---------- 63 | 64 | *The pygls documentation tries to (with varying degrees of success!) follow the* `Diátaxis `__ *approach to writing documentation* 65 | 66 | This documentation site is divided up into the following sections 67 | 68 | .. grid:: 1 2 2 2 69 | :gutter: 2 70 | 71 | .. grid-item-card:: Language Servers 72 | :link: servers/getting-started 73 | :link-type: doc 74 | :text-align: center 75 | 76 | Documentation specific to implementing Language Servers using *pygls*. 77 | 78 | .. grid-item-card:: Language Clients 79 | :text-align: center 80 | 81 | Documentation specific to implementing Language Clients using *pygls*. 82 | Coming Soon\ :sup:`TM`! 83 | 84 | .. grid-item-card:: The Protocol 85 | :link: protocol/howto 86 | :link-type: doc 87 | :text-align: center 88 | 89 | Additional articles that explain some aspect of the Language Server Protocol in general. 90 | 91 | .. grid-item-card:: The Library 92 | :link: pygls/howto 93 | :link-type: doc 94 | :text-align: center 95 | 96 | Documentation that applies to the *pygls* library itself e.g. migration guides. 97 | 98 | .. grid-item-card:: Contributing 99 | :link: contributing/howto 100 | :link-type: doc 101 | :text-align: center 102 | 103 | Guides on how to contribute to *pygls*. 104 | 105 | .. grid-item-card:: About 106 | :text-align: center 107 | 108 | Additional context on the *pygls* project. 109 | 110 | .. _Language Server Protocol: https://microsoft.github.io/language-server-protocol/specification 111 | .. _Language server: https://langserver.org/ 112 | .. _pygls: https://github.com/openlawlibrary/pygls 113 | -------------------------------------------------------------------------------- /docs/source/protocol/howto.rst: -------------------------------------------------------------------------------- 1 | How To 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :glob: 7 | 8 | Interpret Semantic Tokens 9 | -------------------------------------------------------------------------------- /docs/source/protocol/howto/interpret-semantic-tokens.rst: -------------------------------------------------------------------------------- 1 | .. _howto-semantic-tokens: 2 | 3 | How To Interpret Semantic Tokens 4 | ================================ 5 | 6 | .. seealso:: 7 | 8 | :ref:`Example Server ` 9 | An example implementation of semantic tokens 10 | 11 | :lsp:`textDocument/semanticTokens` 12 | Semantic tokens in the LSP Specification 13 | 14 | Semantic Tokens can be thought of as "Syntax Highlighting++". 15 | 16 | Traditional syntax highlighting is usually implemented as a large collection of :mod:`regular expressions ` and can use the language's grammar rules to tell the difference between say a string, variable or function. 17 | 18 | However, regular expressions are not powerful enough to tell if 19 | 20 | - a variable is read-only 21 | - a given function is deprecated 22 | - a class is part of the language's standard library 23 | 24 | This is where the *Semantic* part of Semantic Tokens comes in. 25 | 26 | How are tokens represented? 27 | --------------------------- 28 | 29 | Unlike most parts of the Language Server Protocol, semantic tokens are not represented by a structured object with nicely named fields. 30 | Instead each token is represented by a sequence of 5 integers:: 31 | 32 | [0, 2, 1, 0, 3, 0, 4, 2, 1, 0, ...] 33 | ^-----------^ ^-----------^ 34 | 1st token 2nd token etc. 35 | 36 | In order to explain their meaning, it's probably best to work with an example. 37 | Let's consider the following code snippet:: 38 | 39 | c = sqrt( 40 | a^2 + b^2 41 | ) 42 | 43 | Token Position 44 | -------------- 45 | 46 | The first three numbers are dedicated to encoding a token's posisition in the document. 47 | 48 | The first 2 integers encode the line and character offsets of the token, while the third encodes its length. 49 | The trick however, is that these offsets are **relative to the position of start of the previous token**. 50 | 51 | *Hover over each of the tokens below to see how their offsets are computed* 52 | 53 | .. raw:: html 54 | :file: tokens/positions.html 55 | 56 | Some additional notes 57 | 58 | - For the ``c`` token, there was no previous token so its position is calculated relative to ``(0, 0)`` 59 | - For the tokens ``a`` and ``)``, moving to a new line resets the column offset, so it's calculated relative to ``0`` 60 | 61 | Token Types 62 | ----------- 63 | 64 | The 4th number represents the token's type. 65 | A type indicates if a given token represents a string, variable, function etc. 66 | 67 | When a server declares it supports semantic tokens (as part of the :lsp:`initialize` request) it must send the client a :class:`~lsprotocol.types.SemanticTokensLegend` which includes a list of token types that the server will use. 68 | 69 | .. tip:: 70 | 71 | See :lsp:`semanticTokenTypes` in the specification for a list of all predefiend types. 72 | 73 | To encode a token's type, the 4th number should be set to the index of the corresponding type in the :attr:`SemanticTokensLegend.token_types ` list sent to the client. 74 | 75 | *Hover over each of the tokens below to see their corresponding type* 76 | 77 | .. raw:: html 78 | :file: ./tokens/types.html 79 | 80 | Token Modifiers 81 | --------------- 82 | 83 | So far, we have only managed to re-create traditional syntax highlighting. 84 | It's only with the 5th and final number for the token do we get to the semantic part of semantic tokens. 85 | 86 | Tokens can have zero or more modifiers applied to them that provide additional context for a token, such as marking is as deprecated or read-only. 87 | As with the token types above, a server must include a list of modifiers it is going to use as part of its :class:`~lsprotocol.types.SemanticTokensLegend`. 88 | 89 | .. tip:: 90 | 91 | See :lsp:`semanticTokenModifiers` in the specification for a list of all predefiend modifiers. 92 | 93 | However, since we can provide more than one modifier and we only have one number to do it with, the encoding cannot be as simple as the list index of the modifer(s) we wish to apply. 94 | 95 | To quote the specification: 96 | 97 | .. pull-quote:: 98 | 99 | Since a token type can have n modifiers, multiple token modifiers can be set by using bit flags, so a tokenModifier value of 3 is first viewed as binary ``0b00000011``, which means ``[tokenModifiers[0], tokenModifiers[1]]`` because bits ``0`` and ``1`` are set. 100 | 101 | *Hover over each of the tokens below to see how their modifiers are computed* 102 | 103 | .. raw:: html 104 | :file: ./tokens/modifiers.html 105 | 106 | 107 | Finally! We have managed to construct the values we need to apply semantic tokens to the snippet of code we considered at the start 108 | 109 | .. figure:: ../../../assets/semantic-tokens-example.png 110 | :align: center 111 | 112 | Our semantic tokens example implemented in VSCode 113 | -------------------------------------------------------------------------------- /docs/source/pygls/howto.rst: -------------------------------------------------------------------------------- 1 | How To 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :glob: 7 | 8 | Migrate to v1 9 | Migrate to v2 10 | -------------------------------------------------------------------------------- /docs/source/pygls/reference.rst: -------------------------------------------------------------------------------- 1 | Python API 2 | ========== 3 | 4 | In this section you will find reference documentation on pygls' Python API 5 | 6 | .. toctree:: 7 | :hidden: 8 | :glob: 9 | 10 | reference/* 11 | 12 | 13 | .. grid:: 1 2 2 2 14 | :gutter: 2 15 | 16 | .. grid-item-card:: Clients 17 | :link: reference/clients 18 | :link-type: doc 19 | :text-align: center 20 | 21 | pygls' Language Client APIs. 22 | 23 | .. grid-item-card:: Servers 24 | :link: reference/servers 25 | :link-type: doc 26 | :text-align: center 27 | 28 | pygls' Language Server APIs 29 | 30 | .. grid-item-card:: LSP Types 31 | :link: reference/types 32 | :link-type: doc 33 | :text-align: center 34 | 35 | LSP type definitions, as provided by the *lsprotocol* library 36 | 37 | .. grid-item-card:: URIs 38 | :link: reference/types 39 | :link-type: doc 40 | :text-align: center 41 | 42 | Helper functions for working with URIs 43 | 44 | .. grid-item-card:: Workspace 45 | :link: reference/workspace 46 | :link-type: doc 47 | :text-align: center 48 | 49 | pygls' workspace API 50 | 51 | .. grid-item-card:: Protocol 52 | :link: reference/protocol 53 | :link-type: doc 54 | :text-align: center 55 | 56 | pygls' low-level protocol APIs 57 | 58 | .. grid-item-card:: IO 59 | :link: reference/io 60 | :link-type: doc 61 | :text-align: center 62 | 63 | pygls' low-level input/output APIs 64 | -------------------------------------------------------------------------------- /docs/source/pygls/reference/clients.rst: -------------------------------------------------------------------------------- 1 | Clients 2 | ======= 3 | 4 | .. autoclass:: pygls.lsp.client.BaseLanguageClient 5 | :members: 6 | 7 | .. autoclass:: pygls.client.JsonRPCClient 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/source/pygls/reference/io.rst: -------------------------------------------------------------------------------- 1 | IO 2 | == 3 | 4 | 5 | .. automodule:: pygls.io_ 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/source/pygls/reference/protocol.rst: -------------------------------------------------------------------------------- 1 | Protocol 2 | ======== 3 | 4 | 5 | .. autoclass:: pygls.protocol.LanguageServerProtocol 6 | :members: 7 | 8 | .. autoclass:: pygls.protocol.JsonRPCProtocol 9 | :members: 10 | 11 | .. autofunction:: pygls.protocol.default_converter 12 | -------------------------------------------------------------------------------- /docs/source/pygls/reference/servers.rst: -------------------------------------------------------------------------------- 1 | Servers 2 | ======= 3 | 4 | .. autoclass:: pygls.lsp.server.LanguageServer 5 | :members: 6 | 7 | .. autoclass:: pygls.progress.Progress 8 | :members: 9 | 10 | .. autoclass:: pygls.server.JsonRPCServer 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/source/pygls/reference/types.rst: -------------------------------------------------------------------------------- 1 | Types 2 | ===== 3 | 4 | LSP type definitions in ``pygls`` are provided by the `lsprotocol `__ library 5 | 6 | .. automodule:: lsprotocol.types 7 | :members: 8 | :undoc-members: 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/source/pygls/reference/uris.rst: -------------------------------------------------------------------------------- 1 | URIs 2 | ==== 3 | 4 | .. currentmodule:: pygls.uris 5 | 6 | .. autofunction:: from_fs_path 7 | 8 | .. autofunction:: to_fs_path 9 | 10 | .. autofunction:: uri_scheme 11 | 12 | .. autofunction:: uri_with 13 | 14 | .. autofunction:: urlparse 15 | 16 | .. autofunction:: urlunparse 17 | -------------------------------------------------------------------------------- /docs/source/pygls/reference/workspace.rst: -------------------------------------------------------------------------------- 1 | Workspace 2 | ========= 3 | 4 | .. autoclass:: pygls.workspace.TextDocument 5 | :members: 6 | 7 | .. autoclass:: pygls.workspace.Workspace 8 | :members: 9 | 10 | -------------------------------------------------------------------------------- /docs/source/servers/examples/code-actions.rst: -------------------------------------------------------------------------------- 1 | Code Actions 2 | ============ 3 | 4 | .. example-server:: code_actions.py 5 | :start-at: import re 6 | 7 | -------------------------------------------------------------------------------- /docs/source/servers/examples/code-lens.rst: -------------------------------------------------------------------------------- 1 | Code Lens 2 | ========= 3 | 4 | .. example-server:: code_lens.py 5 | :start-at: import logging 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/servers/examples/colors.rst: -------------------------------------------------------------------------------- 1 | Document Color 2 | ============== 3 | 4 | .. example-server:: colors.py 5 | :start-at: import logging 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/source/servers/examples/formatting.rst: -------------------------------------------------------------------------------- 1 | Document Formatting 2 | =================== 3 | 4 | .. example-server:: formatting.py 5 | :start-at: import logging 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/source/servers/examples/goto.rst: -------------------------------------------------------------------------------- 1 | Goto "X" and Find References 2 | ============================ 3 | 4 | .. example-server:: goto.py 5 | :start-at: import logging 6 | -------------------------------------------------------------------------------- /docs/source/servers/examples/hover.rst: -------------------------------------------------------------------------------- 1 | Hover 2 | ===== 3 | 4 | .. example-server:: hover.py 5 | :start-at: import logging 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/source/servers/examples/inlay-hints.rst: -------------------------------------------------------------------------------- 1 | Inlay Hints 2 | =========== 3 | 4 | .. example-server:: inlay_hints.py 5 | :start-at: import re 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/source/servers/examples/json-server.rst: -------------------------------------------------------------------------------- 1 | JSON Server 2 | =========== 3 | 4 | .. example-server:: json_server.py 5 | :start-at: import asyncio 6 | -------------------------------------------------------------------------------- /docs/source/servers/examples/links.rst: -------------------------------------------------------------------------------- 1 | Document Links 2 | ============== 3 | 4 | .. example-server:: links.py 5 | :start-at: import logging 6 | -------------------------------------------------------------------------------- /docs/source/servers/examples/publish-diagnostics.rst: -------------------------------------------------------------------------------- 1 | Publish Diagnostics 2 | =================== 3 | 4 | .. example-server:: publish_diagnostics.py 5 | :start-at: import logging 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/source/servers/examples/pull-diagnostics.rst: -------------------------------------------------------------------------------- 1 | Pull Diagnostics 2 | ================ 3 | 4 | .. example-server:: pull_diagnostics.py 5 | :start-at: import logging 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/source/servers/examples/rename.rst: -------------------------------------------------------------------------------- 1 | Rename 2 | ====== 3 | 4 | .. example-server:: rename.py 5 | :start-at: import logging 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/source/servers/examples/semantic-tokens.rst: -------------------------------------------------------------------------------- 1 | .. _example-semantic-tokens: 2 | 3 | Semantic Tokens 4 | =============== 5 | 6 | .. example-server:: semantic_tokens.py 7 | :start-at: import enum 8 | -------------------------------------------------------------------------------- /docs/source/servers/examples/symbols.rst: -------------------------------------------------------------------------------- 1 | Document & Workspace Symbols 2 | ============================ 3 | 4 | .. example-server:: symbols.py 5 | :start-at: import logging 6 | -------------------------------------------------------------------------------- /docs/source/servers/examples/threaded-handlers.rst: -------------------------------------------------------------------------------- 1 | Threaded Handlers 2 | ================= 3 | 4 | .. example-server:: threaded_handlers.py 5 | :start-at: import time 6 | -------------------------------------------------------------------------------- /docs/source/servers/getting-started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | .. _example-servers: 5 | 6 | Example Servers 7 | --------------- 8 | 9 | .. toctree:: 10 | :hidden: 11 | :glob: 12 | 13 | examples/* 14 | 15 | .. tip:: 16 | 17 | If you use VSCode, we recommend you try these servers out in the :ref:`pygls-playground ` extension 18 | 19 | Each of the following example servers are focused on implementing a particular subset of the Language Server Protocol. 20 | 21 | .. grid:: 1 2 2 4 22 | :gutter: 2 23 | 24 | .. grid-item-card:: Code Actions 25 | :link: examples/code-actions 26 | :link-type: doc 27 | :text-align: center 28 | 29 | :octicon:`light-bulb` 30 | 31 | .. grid-item-card:: Code Lens 32 | :link: examples/code-lens 33 | :link-type: doc 34 | :text-align: center 35 | 36 | :octicon:`eye` 37 | 38 | .. grid-item-card:: Colors 39 | :link: examples/colors 40 | :link-type: doc 41 | :text-align: center 42 | 43 | :octicon:`paintbrush` 44 | 45 | .. grid-item-card:: Formatting 46 | :link: examples/formatting 47 | :link-type: doc 48 | :text-align: center 49 | 50 | :octicon:`typography` 51 | 52 | .. grid-item-card:: Goto "X" 53 | :link: examples/goto 54 | :link-type: doc 55 | :text-align: center 56 | 57 | :octicon:`search` 58 | 59 | .. grid-item-card:: Hover 60 | :link: examples/hover 61 | :link-type: doc 62 | :text-align: center 63 | 64 | :octicon:`book` 65 | 66 | .. grid-item-card:: Inlay Hints 67 | :link: examples/inlay-hints 68 | :link-type: doc 69 | :text-align: center 70 | 71 | :octicon:`info` 72 | 73 | .. grid-item-card:: Links 74 | :link: examples/links 75 | :link-type: doc 76 | :text-align: center 77 | 78 | :octicon:`link` 79 | 80 | .. grid-item-card:: Publish Diagnostics 81 | :link: examples/publish-diagnostics 82 | :link-type: doc 83 | :text-align: center 84 | 85 | :octicon:`alert` 86 | 87 | .. grid-item-card:: Pull Diagnostics 88 | :link: examples/pull-diagnostics 89 | :link-type: doc 90 | :text-align: center 91 | 92 | :octicon:`alert` 93 | 94 | .. grid-item-card:: Rename 95 | :link: examples/rename 96 | :link-type: doc 97 | :text-align: center 98 | 99 | :octicon:`pencil` 100 | 101 | .. grid-item-card:: Semantic Tokens 102 | :link: examples/semantic-tokens 103 | :link-type: doc 104 | :text-align: center 105 | 106 | :octicon:`file-binary` 107 | 108 | .. grid-item-card:: Symbols 109 | :link: examples/symbols 110 | :link-type: doc 111 | :text-align: center 112 | 113 | :octicon:`code` 114 | 115 | These servers are dedicated to demonstrating features of *pygls* itself 116 | 117 | .. grid:: 1 2 2 4 118 | :gutter: 2 119 | 120 | .. grid-item-card:: JSON Server 121 | :link: examples/json-server 122 | :link-type: doc 123 | :text-align: center 124 | 125 | :octicon:`code` 126 | 127 | .. grid-item-card:: Threaded Handlers 128 | :link: examples/threaded-handlers 129 | :link-type: doc 130 | :text-align: center 131 | 132 | :octicon:`columns` 133 | 134 | 135 | Tutorial 136 | -------- 137 | 138 | .. note:: 139 | 140 | Coming soon\ :sup:`TM` 141 | -------------------------------------------------------------------------------- /docs/source/servers/howto.rst: -------------------------------------------------------------------------------- 1 | How To Guides 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | Handle Invalid Data 8 | Run a Server in Pyodide 9 | Use the pygls-playground 10 | -------------------------------------------------------------------------------- /docs/source/servers/tutorial.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial: 2 | 3 | Tutorial 4 | ======== 5 | 6 | Here we have a tutorial 7 | 8 | .. toctree:: 9 | :glob: 10 | :maxdepth: 1 11 | 12 | tutorial/* 13 | -------------------------------------------------------------------------------- /docs/source/servers/tutorial/0-setup.rst: -------------------------------------------------------------------------------- 1 | Project Setup 2 | ============= 3 | 4 | By the end of this stage you will have everything you need setup in order to follow the rest of this tutorial, including a simple "Hello, World" language server. 5 | 6 | **Required Software** 7 | 8 | Before continuing with the setup you need following software installed: 9 | 10 | * `Visual Studio Code `__ 11 | * `Python 3.8+ `__ 12 | * `Node JS 18+ `__ 13 | * `Git `__ 14 | 15 | Your First Language Server 16 | -------------------------- 17 | -------------------------------------------------------------------------------- /docs/source/servers/tutorial/y-testing.rst: -------------------------------------------------------------------------------- 1 | .. _testing: 2 | 3 | Testing 4 | ======= 5 | 6 | Unit Tests 7 | ---------- 8 | 9 | Writing unit tests for registered features and commands are easy and you don't 10 | have to mock the whole language server. If you skipped the advanced usage page, 11 | take a look at :ref:`passing language server instance ` 12 | section for more details. 13 | 14 | Integration Tests 15 | ----------------- 16 | 17 | Integration tests coverage includes the whole workflow, from sending the client 18 | request, to getting the result from the server. Since the *Language Server 19 | Protocol* defines bidirectional communication between the client and the 20 | server, we used *pygls* to simulate the client and send desired requests to the 21 | server. To get a better understanding of how to set it up, take a look at our test 22 | `fixtures`_. 23 | 24 | .. _fixtures: https://github.com/openlawlibrary/pygls/blob/main/tests/conftest.py 25 | -------------------------------------------------------------------------------- /docs/source/servers/tutorial/z-next-steps.rst: -------------------------------------------------------------------------------- 1 | Next Steps 2 | ========== 3 | 4 | If you decide you want to publish your language server on the VSCode marketplace this 5 | `template extension `__ 6 | from Microsoft a useful starting point. 7 | -------------------------------------------------------------------------------- /examples/hello-world/README.md: -------------------------------------------------------------------------------- 1 | # Hello World Pygls Language Server 2 | 3 | This is the bare-minimum, working example of a Pygls-based Language Server. It is the same as that shown in the main README, it autocompletes `hello.` with the options, "world" and "friend". 4 | 5 | You will only need to have installed Pygls on your system. Eg; `pip install pygls`. Normally you will want to formally define `pygls` as a dependency of your Language Server, with something like [venv](https://docs.python.org/3/library/venv.html), [Poetry](https://python-poetry.org/), etc. 6 | 7 | # Editor Configurations 8 | 9 |
10 | Neovim Lua (vanilla Neovim without `lspconfig`) 11 | 12 | Normally, once you have completed your own Language Server, you will want to submit it to the [LSP Config](https://github.com/neovim/nvim-lspconfig) repo, it is the defacto way to support Language Servers in the Neovim ecosystem. But before then you can just use something like this: 13 | 14 | ```lua 15 | vim.api.nvim_create_autocmd({ "BufEnter" }, { 16 | -- NB: You must remember to manually put the file extension pattern matchers for each LSP filetype 17 | pattern = { "*" }, 18 | callback = function() 19 | vim.lsp.start({ 20 | name = "hello-world-pygls-example", 21 | cmd = { "python path-to-hello-world-example/main.py" }, 22 | root_dir = vim.fs.dirname(vim.fs.find({ ".git" }, { upward = true })[1]) 23 | }) 24 | end, 25 | }) 26 | ``` 27 |
28 | 29 |
30 | Vim (`vim-lsp`) 31 | 32 | ```vim 33 | augroup HelloWorldPythonExample 34 | au! 35 | autocmd User lsp_setup call lsp#register_server({ 36 | \ 'name': 'hello-world-pygls-example', 37 | \ 'cmd': {server_info->['python', 'path-to-hello-world-example/main.py']}, 38 | \ 'allowlist': ['*'] 39 | \ }) 40 | augroup END 41 | ``` 42 |
43 | 44 |
45 | Emacs (`lsp-mode`) 46 | Normally, once your Language Server is complete, you'll want to submit it to the [M-x Eglot](https://github.com/joaotavora/eglot) project, which will automatically set your server up. Until then, you can use: 47 | 48 | ``` 49 | (make-lsp-client :new-connection 50 | (lsp-stdio-connection 51 | `(,(executable-find "python") "path-to-hello-world-example/main.py")) 52 | :activation-fn (lsp-activate-on "*") 53 | :server-id 'hello-world-pygls-example'))) 54 | ``` 55 |
56 | 57 |
58 | Sublime 59 | 60 | 61 | ``` 62 | { 63 | "clients": { 64 | "pygls-hello-world-example": { 65 | "command": ["python", "path-to-hello-world-example/main.py"], 66 | "enabled": true, 67 | "selector": "source.python" 68 | } 69 | } 70 | } 71 | ``` 72 |
73 | 74 |
75 | VSCode 76 | 77 | VSCode is the most complex of the editors to setup. See the [json-vscode-extension](https://github.com/openlawlibrary/pygls/tree/master/examples/json-vscode-extension) for an idea of how to do it. 78 |
79 | -------------------------------------------------------------------------------- /examples/hello-world/main.py: -------------------------------------------------------------------------------- 1 | from pygls.lsp.server import LanguageServer 2 | from lsprotocol import types 3 | 4 | server = LanguageServer("example-server", "v0.1") 5 | 6 | 7 | @server.feature( 8 | types.TEXT_DOCUMENT_COMPLETION, 9 | types.CompletionOptions(trigger_characters=["."]), 10 | ) 11 | def completions(params: types.CompletionParams): 12 | document = server.workspace.get_text_document(params.text_document.uri) 13 | current_line = document.lines[params.position.line].strip() 14 | 15 | if not current_line.endswith("hello."): 16 | return [] 17 | 18 | return [ 19 | types.CompletionItem(label="world"), 20 | types.CompletionItem(label="friend"), 21 | ] 22 | 23 | 24 | if __name__ == "__main__": 25 | server.start_io() 26 | -------------------------------------------------------------------------------- /examples/servers/README.md: -------------------------------------------------------------------------------- 1 | # Example Servers 2 | 3 | | Filename | Works With | Description | 4 | |-|-|-| 5 | | `code_actions.py` | `sums.txt` | Evaluate sums via a code action | 6 | | `code_lens.py` | `sums.txt` | Evaluate sums via a code lens | 7 | | `colors.py` | `colors.txt` | Provides a visual representation of color values and even a color picker in supported clients | 8 | | `formatting.py`| `table.txt`| Implements whole document, selection only and as-you-type formatting for markdown like tables [^1] [^2] | 9 | | `goto.py` | `code.txt` | Implements the various "Goto X" and "Find references" requests in the specification | 10 | | `hover.py` | `dates.txt` | Opens a popup showing the date underneath the cursor in multiple formats | 11 | | `inlay_hints.py` | `sums.txt` | Use inlay hints to show the binary representation of numbers in the file | 12 | | `links.py` | `links.txt` | Implements `textDocument/documentLink` | 13 | | `publish_diagnostics.py` | `sums.txt` | Use "push-model" diagnostics to highlight missing or incorrect answers | 14 | | `pull_diagnostics.py` | `sums.txt` | Use "pull-model" diagnostics to highlight missing or incorrect answers | 15 | | `rename.py` | `code.txt` | Implements symbol renaming | 16 | 17 | 18 | [^1]: To enable as-you-type formatting, be sure to uncomment the `editor.formatOnType` option in `.vscode/settings.json` 19 | 20 | [^2]: This server is enough to demonstrate the bare minimum required to implement these methods be sure to check the contents of the `params` object for all the additional options you shoud be considering! 21 | -------------------------------------------------------------------------------- /examples/servers/async_shutdown.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | import asyncio 18 | import logging 19 | 20 | from lsprotocol import types 21 | 22 | from pygls.cli import start_server 23 | from pygls.lsp.server import LanguageServer 24 | 25 | server = LanguageServer("async-shutdown-server", "v1") 26 | 27 | 28 | @server.feature(types.SHUTDOWN) 29 | async def shutdown(params: None) -> None: 30 | """An async shutdown handler that is long and complicated and takes a while to 31 | complete""" 32 | 33 | logging.info("Shutdown started") 34 | server.window_log_message( 35 | types.LogMessageParams(message="Shutdown started", type=types.MessageType.Info) 36 | ) 37 | 38 | await asyncio.sleep(10) 39 | 40 | server.window_log_message( 41 | types.LogMessageParams(message="Shutdown complete", type=types.MessageType.Info) 42 | ) 43 | logging.info("Shutdown complete") 44 | 45 | 46 | if __name__ == "__main__": 47 | logging.basicConfig(level=logging.INFO, format="%(message)s") 48 | start_server(server) 49 | -------------------------------------------------------------------------------- /examples/servers/code_actions.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | """This example server implements the :lsp:`textDocument/codeAction` request. 18 | 19 | `In VSCode `__ code actions are 20 | typically accessed via a small lightbulb placed near the code the action will affect. 21 | Code actions usually modify the code in some way, usually to fix an error or refactor 22 | it. 23 | 24 | This server scans the document for incomplete sums e.g. ``1 + 1 =`` and returns a code 25 | action which, when invoked will fill in the answer. 26 | """ 27 | 28 | import re 29 | from pygls.cli import start_server 30 | from pygls.lsp.server import LanguageServer 31 | from lsprotocol import types 32 | 33 | 34 | ADDITION = re.compile(r"^\s*(\d+)\s*\+\s*(\d+)\s*=(?=\s*$)") 35 | server = LanguageServer("code-action-server", "v0.1") 36 | 37 | 38 | @server.feature( 39 | types.TEXT_DOCUMENT_CODE_ACTION, 40 | types.CodeActionOptions(code_action_kinds=[types.CodeActionKind.QuickFix]), 41 | ) 42 | def code_actions(params: types.CodeActionParams): 43 | items = [] 44 | document_uri = params.text_document.uri 45 | document = server.workspace.get_text_document(document_uri) 46 | 47 | start_line = params.range.start.line 48 | end_line = params.range.end.line 49 | 50 | lines = document.lines[start_line : end_line + 1] 51 | for idx, line in enumerate(lines): 52 | match = ADDITION.match(line) 53 | if match is not None: 54 | range_ = types.Range( 55 | start=types.Position(line=start_line + idx, character=0), 56 | end=types.Position(line=start_line + idx, character=len(line) - 1), 57 | ) 58 | 59 | left = int(match.group(1)) 60 | right = int(match.group(2)) 61 | answer = left + right 62 | 63 | text_edit = types.TextEdit( 64 | range=range_, new_text=f"{line.strip()} {answer}!" 65 | ) 66 | 67 | action = types.CodeAction( 68 | title=f"Evaluate '{match.group(0)}'", 69 | kind=types.CodeActionKind.QuickFix, 70 | edit=types.WorkspaceEdit(changes={document_uri: [text_edit]}), 71 | ) 72 | items.append(action) 73 | 74 | return items 75 | 76 | 77 | if __name__ == "__main__": 78 | start_server(server) 79 | -------------------------------------------------------------------------------- /examples/servers/hover.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | """This implements the :lsp:`textDocument/hover` request. 18 | 19 | Typically this method will be called when the user places their mouse or cursor over a 20 | symbol in a document, allowing you to provide documentation for the selected symbol. 21 | 22 | This server implements `textDocument/hover` for various datetime representations, 23 | displaying a table how the selected date would be formatted in each of the supported 24 | formats. 25 | """ 26 | 27 | import logging 28 | from datetime import datetime 29 | 30 | from lsprotocol import types 31 | 32 | from pygls.cli import start_server 33 | from pygls.lsp.server import LanguageServer 34 | 35 | DATE_FORMATS = [ 36 | "%H:%M:%S", 37 | "%d/%m/%y", 38 | "%Y-%m-%d", 39 | "%Y-%m-%dT%H:%M:%S", 40 | ] 41 | server = LanguageServer("hover-server", "v1") 42 | 43 | 44 | @server.feature(types.TEXT_DOCUMENT_HOVER) 45 | def hover(ls: LanguageServer, params: types.HoverParams): 46 | pos = params.position 47 | document_uri = params.text_document.uri 48 | document = ls.workspace.get_text_document(document_uri) 49 | 50 | try: 51 | line = document.lines[pos.line] 52 | except IndexError: 53 | return None 54 | 55 | for fmt in DATE_FORMATS: 56 | try: 57 | value = datetime.strptime(line.strip(), fmt) 58 | break 59 | except ValueError: 60 | pass 61 | 62 | else: 63 | # No valid datetime found. 64 | return None 65 | 66 | hover_content = [ 67 | f"# {value.strftime('%a %d %b %Y')}", 68 | "", 69 | "| Format | Value |", 70 | "|:-|-:|", 71 | *[f"| `{fmt}` | {value.strftime(fmt)} |" for fmt in DATE_FORMATS], 72 | ] 73 | 74 | return types.Hover( 75 | contents=types.MarkupContent( 76 | kind=types.MarkupKind.Markdown, 77 | value="\n".join(hover_content), 78 | ), 79 | range=types.Range( 80 | start=types.Position(line=pos.line, character=0), 81 | end=types.Position(line=pos.line + 1, character=0), 82 | ), 83 | ) 84 | 85 | 86 | if __name__ == "__main__": 87 | logging.basicConfig(level=logging.INFO, format="%(message)s") 88 | start_server(server) 89 | -------------------------------------------------------------------------------- /examples/servers/inlay_hints.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | """This implements the :lsp:`textDocument/inlayHint` and :lsp:`inlayHint/resolve` 18 | requests. 19 | 20 | In editors 21 | `like VSCode `__ 22 | inlay hints are often rendered as inline "ghost text". 23 | They are typically used to show the types of variables and return values from functions. 24 | 25 | This server implements ``textDocument/inlayHint`` to scan the given document for integer 26 | values and returns the equivalent representation of that number in binary. 27 | While we could easily compute the inlay hint's tooltip in the same method, this example 28 | uses the ``inlayHint/resolve`` to demonstrate how you can defer expensive computations 29 | to when they are required. 30 | """ 31 | 32 | import re 33 | from typing import Optional 34 | 35 | from lsprotocol import types 36 | from pygls.cli import start_server 37 | from pygls.lsp.server import LanguageServer 38 | 39 | NUMBER = re.compile(r"\d+") 40 | server = LanguageServer("inlay-hint-server", "v1") 41 | 42 | 43 | def parse_int(chars: str) -> Optional[int]: 44 | try: 45 | return int(chars) 46 | except Exception: 47 | return None 48 | 49 | 50 | @server.feature(types.TEXT_DOCUMENT_INLAY_HINT) 51 | def inlay_hints(params: types.InlayHintParams): 52 | items = [] 53 | document_uri = params.text_document.uri 54 | document = server.workspace.get_text_document(document_uri) 55 | 56 | start_line = params.range.start.line 57 | end_line = params.range.end.line 58 | 59 | lines = document.lines[start_line : end_line + 1] 60 | for lineno, line in enumerate(lines): 61 | for match in NUMBER.finditer(line): 62 | if not match: 63 | continue 64 | 65 | number = parse_int(match.group(0)) 66 | if number is None: 67 | continue 68 | 69 | binary_num = bin(number).split("b")[1] 70 | items.append( 71 | types.InlayHint( 72 | label=f":{binary_num}", 73 | kind=types.InlayHintKind.Type, 74 | padding_left=False, 75 | padding_right=True, 76 | position=types.Position(line=lineno, character=match.end()), 77 | ) 78 | ) 79 | 80 | return items 81 | 82 | 83 | @server.feature(types.INLAY_HINT_RESOLVE) 84 | def inlay_hint_resolve(hint: types.InlayHint): 85 | try: 86 | n = int(hint.label[1:], 2) 87 | hint.tooltip = f"Binary representation of the number: {n}" 88 | except Exception: 89 | pass 90 | 91 | return hint 92 | 93 | 94 | if __name__ == "__main__": 95 | start_server(server) 96 | -------------------------------------------------------------------------------- /examples/servers/links.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | """This implements the :lsp:`textDocument/documentLink` and :lsp:`documentLink/resolve` 18 | requests. 19 | 20 | These allow you to add support for custom link syntax to your language. 21 | In editors like VSCode, links will often be underlined and can be opened with a 22 | :kbd:`Ctrl+Click`. 23 | 24 | This server scans the document given to ``textDocument/documentLink`` for the 25 | syntax ```` and returns a document link desribing its location. 26 | While we could easily compute the ``target`` and ``tooltip`` fields in the same 27 | method, this example demonstrates how the ``documentLink/resolve`` method can be used 28 | to defer this until it is actually necessary 29 | """ 30 | 31 | import logging 32 | import re 33 | 34 | from lsprotocol import types 35 | 36 | from pygls.cli import start_server 37 | from pygls.lsp.server import LanguageServer 38 | 39 | LINK = re.compile(r"<(\w+):([^>]+)>") 40 | server = LanguageServer("links-server", "v1") 41 | 42 | 43 | @server.feature( 44 | types.TEXT_DOCUMENT_DOCUMENT_LINK, 45 | ) 46 | def document_links(params: types.DocumentLinkParams): 47 | """Return a list of links contained in the document.""" 48 | items = [] 49 | document_uri = params.text_document.uri 50 | document = server.workspace.get_text_document(document_uri) 51 | 52 | for linum, line in enumerate(document.lines): 53 | for match in LINK.finditer(line): 54 | start_char, end_char = match.span() 55 | items.append( 56 | types.DocumentLink( 57 | range=types.Range( 58 | start=types.Position(line=linum, character=start_char), 59 | end=types.Position(line=linum, character=end_char), 60 | ), 61 | data={"type": match.group(1), "target": match.group(2)}, 62 | ), 63 | ) 64 | 65 | return items 66 | 67 | 68 | LINK_TYPES = { 69 | "github": ("https://github.com/{}", "Github - {}"), 70 | "pypi": ("https://pypi.org/project/{}", "PyPi - {}"), 71 | } 72 | 73 | 74 | @server.feature(types.DOCUMENT_LINK_RESOLVE) 75 | def document_link_resolve(link: types.DocumentLink): 76 | """Given a link, fill in additional information about it""" 77 | logging.info("resolving link: %s", link) 78 | 79 | link_type = link.data.get("type", "") 80 | link_target = link.data.get("target", "") 81 | 82 | if (link_info := LINK_TYPES.get(link_type, None)) is None: 83 | logging.error("Unknown link type: '%s'", link_type) 84 | return link 85 | 86 | url, tooltip = link_info 87 | link.target = url.format(link_target) 88 | link.tooltip = tooltip.format(link_target) 89 | 90 | return link 91 | 92 | 93 | if __name__ == "__main__": 94 | logging.basicConfig(level=logging.INFO, format="%(message)s") 95 | start_server(server) 96 | -------------------------------------------------------------------------------- /examples/servers/register_during_initialize.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | import logging 18 | 19 | from lsprotocol import types 20 | 21 | from pygls.cli import start_server 22 | from pygls.lsp.server import LanguageServer 23 | 24 | server = LanguageServer("register-during-init-server", "v1") 25 | 26 | 27 | @server.feature(types.INITIALIZE) 28 | def initialize(params: types.InitializeParams): 29 | """An initialize handler that only registers a ``textDocument/formatting`` handler 30 | if the user requests it in their initialzation options.""" 31 | 32 | init_options = params.initialization_options or {} 33 | if init_options.get("formatting", False): 34 | 35 | @server.feature(types.TEXT_DOCUMENT_FORMATTING) 36 | async def format_document( 37 | ls: LanguageServer, params: types.DocumentFormattingParams 38 | ): 39 | return None 40 | 41 | 42 | if __name__ == "__main__": 43 | logging.basicConfig(level=logging.INFO, format="%(message)s") 44 | start_server(server) 45 | -------------------------------------------------------------------------------- /examples/servers/threaded_handlers.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | """This example server demonstrates pygls' ability to run message handlers in a separate 18 | thread. 19 | 20 | """ 21 | from __future__ import annotations 22 | 23 | import time 24 | import threading 25 | 26 | from lsprotocol import types 27 | 28 | from pygls.cli import start_server 29 | from pygls.lsp.server import LanguageServer 30 | 31 | server = LanguageServer("threaded-server", "v1") 32 | 33 | 34 | @server.feature( 35 | types.TEXT_DOCUMENT_COMPLETION, 36 | types.CompletionOptions(trigger_characters=["."]), 37 | ) 38 | def completions(params: types.CompletionParams | None = None) -> types.CompletionList: 39 | """Returns completion items.""" 40 | return types.CompletionList( 41 | is_incomplete=False, 42 | items=[ 43 | types.CompletionItem(label="one"), 44 | types.CompletionItem(label="two"), 45 | types.CompletionItem(label="three"), 46 | types.CompletionItem(label="four"), 47 | types.CompletionItem(label="five"), 48 | ], 49 | ) 50 | 51 | 52 | @server.command("count.down.blocking") 53 | def count_down_blocking(ls: LanguageServer, *args): 54 | """Starts counting down and showing message synchronously. 55 | It will block the main thread, which can be tested by trying to show 56 | completion items. 57 | """ 58 | thread = threading.current_thread() 59 | for i in range(10): 60 | ls.window_show_message( 61 | types.ShowMessageParams( 62 | message=f"Counting down in thread {thread.name!r} ... {10 - i}", 63 | type=types.MessageType.Info, 64 | ), 65 | ) 66 | time.sleep(1) 67 | 68 | 69 | @server.thread() 70 | @server.command("count.down.thread") 71 | def count_down_thread(ls: LanguageServer, *args): 72 | """Starts counting down and showing messages in a separate thread. 73 | It will NOT block the main thread, which can be tested by trying to show 74 | completion items. 75 | """ 76 | thread = threading.current_thread() 77 | 78 | for i in range(10): 79 | ls.window_show_message( 80 | types.ShowMessageParams( 81 | message=f"Counting down in thread {thread.name!r} ... {10 - i}", 82 | type=types.MessageType.Info, 83 | ), 84 | ) 85 | time.sleep(1) 86 | 87 | 88 | @server.thread() 89 | @server.command("count.down.error") 90 | def count_down_error(ls: LanguageServer, *args): 91 | """A threaded handler that throws an error.""" 92 | 1 / 0 93 | 94 | 95 | if __name__ == "__main__": 96 | start_server(server) 97 | -------------------------------------------------------------------------------- /examples/servers/workspace/Untitled-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "12\n", 10 | "#" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": { 17 | "mykey": 3 18 | }, 19 | "outputs": [], 20 | "source": [ 21 | "#" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [] 28 | } 29 | ], 30 | "metadata": { 31 | "kernelspec": { 32 | "display_name": "Python 3", 33 | "language": "python", 34 | "name": "python3" 35 | }, 36 | "language_info": { 37 | "name": "python", 38 | "version": "3.11.4" 39 | }, 40 | "orig_nbformat": 4 41 | }, 42 | "nbformat": 4, 43 | "nbformat_minor": 2 44 | } 45 | -------------------------------------------------------------------------------- /examples/servers/workspace/code.txt: -------------------------------------------------------------------------------- 1 | type Rectangle(x, y, w, h) = {x: x, y: y, width: w, height: h } 2 | type Square(x, y, s) = Rectangle(x, y, s, s) 3 | 4 | fn area(rect: Rectangle) -> rect.width * rect.height 5 | 6 | fn volume(rect: Rectangle, length: float) -> area(rect) * length 7 | -------------------------------------------------------------------------------- /examples/servers/workspace/colors.txt: -------------------------------------------------------------------------------- 1 | red is #ff0000 green is #00ff00 and blue is #0000ff 2 | some more colors are below 3 | 4 | yellow #ffff00 5 | 6 | pink #ff00ff 7 | 8 | cyan #00ffff 9 | 10 | short form colors are recognised too, e.g. #f00, #0f0, #00f 11 | 12 | hover over a color to reveal a color picker! 13 | -------------------------------------------------------------------------------- /examples/servers/workspace/dates.txt: -------------------------------------------------------------------------------- 1 | 01/02/20 2 | 3 | 1921-01-02T23:59:00 4 | -------------------------------------------------------------------------------- /examples/servers/workspace/links.txt: -------------------------------------------------------------------------------- 1 | pygls is a framework for writing language servers in Python! 2 | It can be installed from PyPi , it depends on the lsprotocol package 3 | -------------------------------------------------------------------------------- /examples/servers/workspace/sums.txt: -------------------------------------------------------------------------------- 1 | 1 + 1 = 2 | 3 | 4 | 2 + 3 = 5 | 6 | 7 | 6 + 6 = 8 | -------------------------------------------------------------------------------- /examples/servers/workspace/table.txt: -------------------------------------------------------------------------------- 1 | |a|b| 2 | |-|-| 3 | |apple|banana| 4 | -------------------------------------------------------------------------------- /examples/servers/workspace/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value" 3 | } 4 | -------------------------------------------------------------------------------- /pygls/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Original work Copyright 2018 Palantir Technologies, Inc. # 3 | # Original work licensed under the MIT License. # 4 | # See ThirdPartyNotices.txt in the project root for license information. # 5 | # All modifications Copyright (c) Open Law Library. All rights reserved. # 6 | # # 7 | # Licensed under the Apache License, Version 2.0 (the "License") # 8 | # you may not use this file except in compliance with the License. # 9 | # You may obtain a copy of the License at # 10 | # # 11 | # http: // www.apache.org/licenses/LICENSE-2.0 # 12 | # # 13 | # Unless required by applicable law or agreed to in writing, software # 14 | # distributed under the License is distributed on an "AS IS" BASIS, # 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 16 | # See the License for the specific language governing permissions and # 17 | # limitations under the License. # 18 | ############################################################################ 19 | import os 20 | import sys 21 | 22 | IS_WIN = os.name == "nt" 23 | IS_PYODIDE = "pyodide" in sys.modules 24 | IS_WASI = sys.platform == "wasi" 25 | IS_WASM = IS_PYODIDE or IS_WASI 26 | 27 | pygls = "pygls" 28 | -------------------------------------------------------------------------------- /pygls/cli.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | """A simple cli wrapper for pygls servers.""" 18 | from __future__ import annotations 19 | 20 | import argparse 21 | import typing 22 | 23 | if typing.TYPE_CHECKING: 24 | from pygls.server import JsonRPCServer 25 | 26 | 27 | def start_server(server: JsonRPCServer, args: list[str] | None = None): 28 | """A helper function that implements a simple cli wrapper for a pygls server 29 | allowing the user to select between the supported transports.""" 30 | 31 | name = type(server).__name__ 32 | parser = argparse.ArgumentParser(description=f"start a {name} instance") 33 | parser.add_argument("--tcp", action="store_true", help="start a TCP server") 34 | parser.add_argument("--ws", action="store_true", help="start a WebSocket server") 35 | parser.add_argument("--host", default="127.0.0.1", help="bind to this address") 36 | parser.add_argument("--port", type=int, default=8888, help="bind to this port") 37 | 38 | arguments = parser.parse_args(args) 39 | 40 | if arguments.tcp: 41 | server.start_tcp(arguments.host, arguments.port) 42 | elif arguments.ws: 43 | server.start_ws(arguments.host, arguments.port) 44 | else: 45 | server.start_io() 46 | -------------------------------------------------------------------------------- /pygls/constants.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | 18 | # Dynamically assigned attributes 19 | ATTR_EXECUTE_IN_THREAD = "execute_in_thread" 20 | ATTR_COMMAND_TYPE = "command" 21 | ATTR_FEATURE_TYPE = "feature" 22 | ATTR_REGISTERED_NAME = "reg_name" 23 | ATTR_REGISTERED_TYPE = "reg_type" 24 | 25 | # Parameters 26 | PARAM_LS = "ls" 27 | -------------------------------------------------------------------------------- /pygls/lsp/client.py: -------------------------------------------------------------------------------- 1 | from ._base_client import BaseLanguageClient 2 | 3 | 4 | # Placeholder for when we add a real client 5 | class LanguageClient(BaseLanguageClient): 6 | """Language client.""" 7 | -------------------------------------------------------------------------------- /pygls/lsp/server.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | from lsprotocol import types 6 | 7 | from pygls.exceptions import FeatureRequestError 8 | 9 | from ._base_server import BaseLanguageServer 10 | 11 | if typing.TYPE_CHECKING: 12 | from typing import Callable 13 | from typing import TypeVar 14 | 15 | from pygls.server import ServerErrors 16 | from pygls.progress import Progress 17 | from pygls.workspace import Workspace 18 | 19 | F = TypeVar("F", bound=Callable) 20 | 21 | 22 | class LanguageServer(BaseLanguageServer): 23 | """The default LanguageServer 24 | 25 | This class can be extended and it can be passed as a first argument to 26 | registered commands/features. 27 | 28 | .. |ServerInfo| replace:: :class:`~lsprotocol.types.ServerInfo` 29 | 30 | Parameters 31 | ---------- 32 | name 33 | Name of the server, used to populate |ServerInfo| which is sent to 34 | the client during initialization 35 | 36 | version 37 | Version of the server, used to populate |ServerInfo| which is sent to 38 | the client during initialization 39 | 40 | protocol_cls 41 | The :class:`~pygls.protocol.LanguageServerProtocol` class definition, or any 42 | subclass of it. 43 | 44 | max_workers 45 | Maximum number of workers for ``ThreadPool`` and ``ThreadPoolExecutor`` 46 | 47 | text_document_sync_kind 48 | Text document synchronization method 49 | 50 | None 51 | No synchronization 52 | 53 | :attr:`~lsprotocol.types.TextDocumentSyncKind.Full` 54 | Send entire document text with each update 55 | 56 | :attr:`~lsprotocol.types.TextDocumentSyncKind.Incremental` 57 | Send only the region of text that changed with each update 58 | 59 | notebook_document_sync 60 | Advertise :lsp:`NotebookDocument` support to the client. 61 | """ 62 | 63 | def __init__( 64 | self, 65 | name: str, 66 | version: str, 67 | text_document_sync_kind: types.TextDocumentSyncKind = types.TextDocumentSyncKind.Incremental, 68 | notebook_document_sync: types.NotebookDocumentSyncOptions | None = None, 69 | *args, 70 | **kwargs, 71 | ): 72 | self.name = name 73 | self.version = version 74 | self._text_document_sync_kind = text_document_sync_kind 75 | self._notebook_document_sync = notebook_document_sync 76 | self.process_id: int | None = None 77 | super().__init__(*args, **kwargs) 78 | 79 | @property 80 | def client_capabilities(self) -> types.ClientCapabilities: 81 | """The client's capabilities.""" 82 | return self.protocol.client_capabilities 83 | 84 | @property 85 | def server_capabilities(self) -> types.ServerCapabilities: 86 | """The server's capabilities.""" 87 | return self.protocol.server_capabilities 88 | 89 | @property 90 | def workspace(self) -> Workspace: 91 | """Returns in-memory workspace.""" 92 | return self.protocol.workspace 93 | 94 | @property 95 | def work_done_progress(self) -> Progress: 96 | """Gets the object to manage client's progress bar.""" 97 | return self.protocol.progress 98 | 99 | def report_server_error(self, error: Exception, source: ServerErrors): 100 | """ 101 | Sends error to the client for displaying. 102 | 103 | By default this function does not handle LSP request errors. This is because LSP requests 104 | require direct responses and so already have a mechanism for including unexpected errors 105 | in the response body. 106 | 107 | All other errors are "out of band" in the sense that the client isn't explicitly waiting 108 | for them. For example diagnostics are returned as notifications, not responses to requests, 109 | and so can seemingly be sent at random. Also for example consider JSON RPC serialization 110 | and deserialization, if a payload cannot be parsed then the whole request/response cycle 111 | cannot be completed and so one of these "out of band" error messages is sent. 112 | 113 | These "out of band" error messages are not a requirement of the LSP spec. Pygls simply 114 | offers this behaviour as a recommended default. It is perfectly reasonble to override this 115 | default. 116 | """ 117 | 118 | if source == FeatureRequestError: 119 | return 120 | 121 | self.window_show_message( 122 | types.ShowMessageParams( 123 | message=f"Error in server: {error}", 124 | type=types.MessageType.Error, 125 | ) 126 | ) 127 | -------------------------------------------------------------------------------- /pygls/progress.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from concurrent.futures import Future 3 | from typing import Dict 4 | 5 | from lsprotocol.types import ( 6 | PROGRESS, 7 | WINDOW_WORK_DONE_PROGRESS_CREATE, 8 | ProgressParams, 9 | ProgressToken, 10 | WorkDoneProgressBegin, 11 | WorkDoneProgressEnd, 12 | WorkDoneProgressReport, 13 | WorkDoneProgressCreateParams, 14 | ) 15 | from pygls.protocol import LanguageServerProtocol 16 | 17 | 18 | class Progress: 19 | """A class for working with client's progress bar. 20 | 21 | Attributes: 22 | _lsp(LanguageServerProtocol): Language server protocol instance 23 | tokens(dict): Holds futures for work done progress tokens that are 24 | already registered. These futures will be cancelled if the client 25 | sends a cancel work done process notification. 26 | """ 27 | 28 | def __init__(self, lsp: LanguageServerProtocol) -> None: 29 | self._lsp = lsp 30 | 31 | self.tokens: Dict[ProgressToken, Future] = {} 32 | 33 | def _check_token_registered(self, token: ProgressToken) -> None: 34 | if token in self.tokens: 35 | raise Exception("Token is already registered!") 36 | 37 | def _register_token(self, token: ProgressToken) -> None: 38 | self.tokens[token] = Future() 39 | 40 | def create(self, token: ProgressToken, callback=None) -> Future: 41 | """Create a server initiated work done progress.""" 42 | self._check_token_registered(token) 43 | 44 | def on_created(*args, **kwargs): 45 | self._register_token(token) 46 | if callback is not None: 47 | callback(*args, **kwargs) 48 | 49 | return self._lsp.send_request( 50 | WINDOW_WORK_DONE_PROGRESS_CREATE, 51 | WorkDoneProgressCreateParams(token=token), 52 | on_created, 53 | ) 54 | 55 | async def create_async(self, token: ProgressToken) -> asyncio.Future: 56 | """Create a server initiated work done progress.""" 57 | self._check_token_registered(token) 58 | 59 | result = await self._lsp.send_request_async( 60 | WINDOW_WORK_DONE_PROGRESS_CREATE, 61 | WorkDoneProgressCreateParams(token=token), 62 | ) 63 | self._register_token(token) 64 | return result 65 | 66 | def begin(self, token: ProgressToken, value: WorkDoneProgressBegin) -> None: 67 | """Notify beginning of work.""" 68 | # Register cancellation future for the case of client initiated progress 69 | self.tokens.setdefault(token, Future()) 70 | 71 | return self._lsp.notify(PROGRESS, ProgressParams(token=token, value=value)) 72 | 73 | def report(self, token: ProgressToken, value: WorkDoneProgressReport) -> None: 74 | """Notify progress of work.""" 75 | self._lsp.notify(PROGRESS, ProgressParams(token=token, value=value)) 76 | 77 | def end(self, token: ProgressToken, value: WorkDoneProgressEnd) -> None: 78 | """Notify end of work.""" 79 | self._lsp.notify(PROGRESS, ProgressParams(token=token, value=value)) 80 | -------------------------------------------------------------------------------- /pygls/protocol/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections import namedtuple 3 | from typing import Any 4 | 5 | from lsprotocol import converters 6 | 7 | from pygls.protocol.json_rpc import ( 8 | JsonRPCNotification, 9 | JsonRPCProtocol, 10 | JsonRPCRequestMessage, 11 | JsonRPCResponseMessage, 12 | ) 13 | from pygls.protocol.language_server import LanguageServerProtocol, lsp_method 14 | 15 | 16 | def _dict_to_object(d: Any): 17 | """Create nested objects (namedtuple) from dict.""" 18 | 19 | if d is None: 20 | return None 21 | 22 | if not isinstance(d, dict): 23 | return d 24 | 25 | type_name = d.pop("type_name", "Object") 26 | return json.loads( 27 | json.dumps(d), 28 | object_hook=lambda p: namedtuple(type_name, p.keys(), rename=True)(*p.values()), 29 | ) 30 | 31 | 32 | def _params_field_structure_hook(obj, cls): 33 | if "params" in obj: 34 | obj["params"] = _dict_to_object(obj["params"]) 35 | 36 | return cls(**obj) 37 | 38 | 39 | def _result_field_structure_hook(obj, cls): 40 | if "result" in obj: 41 | obj["result"] = _dict_to_object(obj["result"]) 42 | 43 | return cls(**obj) 44 | 45 | 46 | def default_converter(): 47 | """Default converter factory function.""" 48 | 49 | converter = converters.get_converter() 50 | converter.register_structure_hook( 51 | JsonRPCRequestMessage, _params_field_structure_hook 52 | ) 53 | 54 | converter.register_structure_hook( 55 | JsonRPCResponseMessage, _result_field_structure_hook 56 | ) 57 | 58 | converter.register_structure_hook(JsonRPCNotification, _params_field_structure_hook) 59 | 60 | return converter 61 | 62 | 63 | __all__ = ( 64 | "JsonRPCProtocol", 65 | "LanguageServerProtocol", 66 | "JsonRPCRequestMessage", 67 | "JsonRPCResponseMessage", 68 | "JsonRPCNotification", 69 | "_dict_to_object", 70 | "_params_field_structure_hook", 71 | "_result_field_structure_hook", 72 | "default_converter", 73 | "lsp_method", 74 | ) 75 | -------------------------------------------------------------------------------- /pygls/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. The pygls package uses inline types. 2 | 3 | -------------------------------------------------------------------------------- /pygls/workspace/__init__.py: -------------------------------------------------------------------------------- 1 | from .workspace import Workspace 2 | from .text_document import TextDocument 3 | from .position_codec import PositionCodec 4 | 5 | __all__ = ( 6 | "Workspace", 7 | "TextDocument", 8 | "PositionCodec", 9 | ) 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pygls" 3 | version = "2.0.0a4" 4 | description = "A pythonic generic language server (pronounced like 'pie glass')" 5 | authors = ["Open Law Library "] 6 | maintainers = [ 7 | "Tom BH ", 8 | "Alex Carney ", 9 | ] 10 | repository = "https://github.com/openlawlibrary/pygls" 11 | documentation = "https://pygls.readthedocs.io/en/latest" 12 | license = "Apache-2.0" 13 | readme = "README.md" 14 | 15 | # You may want to use the Poetry "Up" plugin to automatically update all dependencies to 16 | # their latest major versions. But bear in mind that this is a library, so the non-development 17 | # dependency versions will be forced on downstream users. Therefore the very latest versions 18 | # may be too restrictive. 19 | # See https://github.com/MousaZeidBaker/poetry-plugin-up 20 | [tool.poetry.dependencies] 21 | python = ">=3.9" 22 | attrs = ">=24.3.0" 23 | cattrs = ">=23.1.2" 24 | lsprotocol = "2025.0.0rc1" 25 | websockets = { version = ">=13.0", optional = true } 26 | 27 | [tool.poetry.extras] 28 | ws = ["websockets"] 29 | 30 | [tool.poetry.group.dev.dependencies] 31 | # Replaces (amongst many other things) flake8 and bandit 32 | ruff = ">=0.1.6" 33 | poethepoet = ">=0.24.4" 34 | mypy = ">=1.7.1" 35 | black = "^24.4.1" 36 | 37 | [tool.poetry.group.test.dependencies] 38 | # Note: `coverage` requires that your Python was built with system `sqlite` development files 39 | coverage = { version = ">=7.3.2", extras = ["toml"] } 40 | pytest = ">=7.4.3" 41 | pytest-asyncio = ">=0.21.0" 42 | pytest-cov = ">=5" 43 | 44 | [tool.poetry.group.docs.dependencies] 45 | # TODO `sphinx>=7.26` needs python 3.9 46 | myst-parser = ">=2.0" 47 | sphinx = ">=7.1.2" 48 | sphinx-design = ">=0.5.0" 49 | sphinx-rtd-theme = ">=1.3.0" 50 | 51 | [tool.pytest.ini_options] 52 | asyncio_mode = "auto" 53 | asyncio_default_fixture_loop_scope = "function" 54 | 55 | [tool.poe.tasks] 56 | test-pyodide = "pytest tests/e2e --lsp-runtime pyodide" 57 | ruff = "ruff check ." 58 | mypy = "mypy -p pygls" 59 | check_generated_code = "python scripts/check_generated_code_is_uptodate.py" 60 | check_commit_style = "npx commitlint --from origin/main --to HEAD --verbose --config commitlintrc.yaml" 61 | generate_code = "python scripts/generate_code.py pygls/lsp" 62 | generate_contributors_md = "python scripts/generate_contributors_md.py" 63 | black_check = "black --check ." 64 | poetry_lock_check = "poetry check" 65 | 66 | [tool.poe.tasks.test] 67 | sequence = [ 68 | { cmd = "pytest --cov" }, 69 | { cmd = "pytest tests/e2e --lsp-transport tcp" }, 70 | { cmd = "pytest tests/e2e --lsp-transport websockets" }, 71 | ] 72 | ignore_fail = "return_non_zero" 73 | 74 | [tool.poe.tasks.lint] 75 | sequence = [ 76 | "ruff", 77 | "mypy", 78 | "check_generated_code", 79 | "check_commit_style", 80 | "black_check", 81 | "poetry_lock_check" 82 | ] 83 | ignore_fail = "return_non_zero" 84 | 85 | [tool.pyright] 86 | strict = ["pygls"] 87 | 88 | [tool.ruff] 89 | # Sometimes Black can't reduce line length without breaking more imortant rules. 90 | # So allow Ruff to be more lenient. 91 | line-length = 120 92 | 93 | [tool.black] 94 | line-length = 88 95 | extend-exclude = "pygls/lsp/_base_.*.py|pygls/lsp/_capabilities.py" 96 | 97 | [tool.coverage.run] 98 | parallel = true 99 | source_pkgs = ["pygls"] 100 | 101 | [tool.coverage.report] 102 | show_missing = true 103 | skip_covered = true 104 | sort = "Cover" 105 | 106 | [tool.mypy] 107 | check_untyped_defs = true 108 | 109 | [build-system] 110 | requires = ["poetry-core"] 111 | build-backend = "poetry.core.masonry.api" 112 | -------------------------------------------------------------------------------- /scripts/check_generated_code_is_uptodate.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | 4 | AUTOGENERATED_CLIENT_FILE = "pygls/lsp/_base_client.py" 5 | AUTOGENERATED_SERVER_FILE = "pygls/lsp/_base_server.py" 6 | 7 | subprocess.run(["poe", "generate_code"]) 8 | 9 | result = subprocess.run( 10 | [ 11 | "git", 12 | "diff", 13 | "--exit-code", 14 | AUTOGENERATED_CLIENT_FILE, 15 | AUTOGENERATED_SERVER_FILE, 16 | ], 17 | stdout=subprocess.DEVNULL, 18 | ) 19 | 20 | if result.returncode == 0: 21 | print("✅ Pygls client and server are up to date") 22 | else: 23 | print( 24 | ( 25 | "🔴 Pygls client or server not up to date\n" 26 | "1. Re-generate with: `poetry run poe generate_code`\n" 27 | "2. Commit" 28 | ) 29 | ) 30 | sys.exit(result.returncode) 31 | -------------------------------------------------------------------------------- /scripts/generate_contributors_md.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example JSON object: 3 | { 4 | "login": "danixeee", 5 | "id": 16227576, 6 | "node_id": "MDQ6VXNlcjE2MjI3NTc2", 7 | "avatar_url": "https://avatars.githubusercontent.com/u/16227576?v=4", 8 | "gravatar_id": "", 9 | "url": "https://api.github.com/users/danixeee", 10 | "html_url": "https://github.com/danixeee", 11 | "followers_url": "https://api.github.com/users/danixeee/followers", 12 | "following_url": "https://api.github.com/users/danixeee/following{/other_user}", 13 | "gists_url": "https://api.github.com/users/danixeee/gists{/gist_id}", 14 | "starred_url": "https://api.github.com/users/danixeee/starred{/owner}{/repo}", 15 | "subscriptions_url": "https://api.github.com/users/danixeee/subscriptions", 16 | "organizations_url": "https://api.github.com/users/danixeee/orgs", 17 | "repos_url": "https://api.github.com/users/danixeee/repos", 18 | "events_url": "https://api.github.com/users/danixeee/events{/privacy}", 19 | "received_events_url": "https://api.github.com/users/danixeee/received_events", 20 | "type": "User", 21 | "site_admin": false, 22 | "contributions": 321 23 | } 24 | """ 25 | 26 | import requests 27 | 28 | PYGLS_CONTRIBUTORS_JSON_URL = ( 29 | "https://api.github.com/repos/openlawlibrary/pygls/contributors" 30 | ) 31 | CONTRIBUTORS_FILE = "CONTRIBUTORS.md" 32 | 33 | response = requests.get(PYGLS_CONTRIBUTORS_JSON_URL) 34 | contributors = sorted(response.json(), key=lambda d: d["login"].lower()) 35 | 36 | contents = "# Contributors (contributions)\n" 37 | 38 | for contributor in contributors: 39 | name = contributor["login"] 40 | contributions = contributor["contributions"] 41 | url = contributor["html_url"] 42 | contents += f"* [{name}]({url}) ({contributions})\n" 43 | 44 | file = open(CONTRIBUTORS_FILE, "w") 45 | n = file.write(contents) 46 | file.close() 47 | 48 | print("✅ CONTRIBUTORS.md updated") 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Original work Copyright 2017 Palantir Technologies, Inc. # 3 | # Original work licensed under the MIT License. # 4 | # See ThirdPartyNotices.txt in the project root for license information. # 5 | # All modifications Copyright (c) Open Law Library. All rights reserved. # 6 | # # 7 | # Licensed under the Apache License, Version 2.0 (the "License") # 8 | # you may not use this file except in compliance with the License. # 9 | # You may obtain a copy of the License at # 10 | # # 11 | # http: // www.apache.org/licenses/LICENSE-2.0 # 12 | # # 13 | # Unless required by applicable law or agreed to in writing, software # 14 | # distributed under the License is distributed on an "AS IS" BASIS, # 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 16 | # See the License for the specific language governing permissions and # 17 | # limitations under the License. # 18 | ############################################################################ 19 | import pytest 20 | 21 | from pygls import IS_WIN 22 | 23 | unix_only = pytest.mark.skipif(IS_WIN, reason="Unix only") 24 | windows_only = pytest.mark.skipif(not IS_WIN, reason="Windows only") 25 | 26 | CMD_ASYNC = "cmd_async" 27 | CMD_SYNC = "cmd_sync" 28 | CMD_THREAD = "cmd_thread" 29 | -------------------------------------------------------------------------------- /tests/_init_server_stall_fix_hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | It would be great to find the real underlying issue here, but without these 3 | retries we get annoying flakey test errors. So it's preferable to hack this 4 | fix to actually guarantee it doesn't generate false negatives in the test 5 | suite. 6 | """ 7 | 8 | import os 9 | import concurrent 10 | 11 | RETRIES = 3 12 | 13 | 14 | def retry_stalled_init_fix_hack(): 15 | if "DISABLE_TIMEOUT" in os.environ: 16 | return lambda f: f 17 | 18 | def decorator(func): 19 | def newfn(*args, **kwargs): 20 | attempt = 0 21 | while attempt < RETRIES: 22 | try: 23 | return func(*args, **kwargs) 24 | except concurrent.futures._base.TimeoutError: 25 | print( 26 | "\n\nRetrying timeouted test server init " 27 | "%d of %d\n" % (attempt, RETRIES) 28 | ) 29 | attempt += 1 30 | return func(*args, **kwargs) 31 | 32 | return newfn 33 | 34 | return decorator 35 | -------------------------------------------------------------------------------- /tests/e2e/test_async_shutdown.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest 22 | import pytest_asyncio 23 | from lsprotocol import types 24 | 25 | if typing.TYPE_CHECKING: 26 | from typing import Tuple 27 | 28 | from pygls.lsp.client import BaseLanguageClient 29 | 30 | 31 | @pytest_asyncio.fixture() 32 | async def async_shutdown(runtime: str, get_client_for): 33 | if runtime in {"pyodide"}: 34 | pytest.skip("async handlers not supported in this runtime") 35 | 36 | async for result in get_client_for("async_shutdown.py", auto_shutdown=False): 37 | client = result[0] 38 | client.log_messages = [] 39 | 40 | @client.feature(types.WINDOW_LOG_MESSAGE) 41 | def _(params: types.LogMessageParams): 42 | client.log_messages.append(params.message) 43 | 44 | yield result 45 | 46 | 47 | async def test_async_shutdown( 48 | async_shutdown: Tuple[BaseLanguageClient, types.InitializeResult], uri_for 49 | ): 50 | """Ensure that the formatting provider is not set by default.""" 51 | client, _ = async_shutdown 52 | 53 | await client.shutdown_async(None) 54 | client.exit(None) 55 | 56 | assert client.log_messages[0] == "Shutdown started" 57 | assert client.log_messages[1] == "Shutdown complete" 58 | -------------------------------------------------------------------------------- /tests/e2e/test_code_action.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest_asyncio 22 | from lsprotocol import types 23 | 24 | if typing.TYPE_CHECKING: 25 | from typing import Tuple 26 | 27 | from pygls.lsp.client import BaseLanguageClient 28 | 29 | 30 | @pytest_asyncio.fixture() 31 | async def code_actions(get_client_for): 32 | async for result in get_client_for("code_actions.py"): 33 | yield result 34 | 35 | 36 | async def test_code_actions( 37 | code_actions: Tuple[BaseLanguageClient, types.InitializeResult], uri_for 38 | ): 39 | """Ensure that the example code action server is working as expected.""" 40 | client, initialize_result = code_actions 41 | 42 | code_action_options = initialize_result.capabilities.code_action_provider 43 | assert code_action_options.code_action_kinds == [types.CodeActionKind.QuickFix] 44 | 45 | test_uri = uri_for("sums.txt") 46 | assert test_uri is not None 47 | 48 | response = await client.text_document_code_action_async( 49 | types.CodeActionParams( 50 | text_document=types.TextDocumentIdentifier(uri=test_uri), 51 | range=types.Range( 52 | start=types.Position(line=0, character=0), 53 | end=types.Position(line=1, character=0), 54 | ), 55 | context=types.CodeActionContext(diagnostics=[]), 56 | ) 57 | ) 58 | 59 | assert len(response) == 1 60 | code_action = response[0] 61 | 62 | assert code_action.title == "Evaluate '1 + 1 ='" 63 | assert code_action.kind == types.CodeActionKind.QuickFix 64 | 65 | fix = code_action.edit.changes[test_uri][0] 66 | expected_range = types.Range( 67 | start=types.Position(line=0, character=0), 68 | end=types.Position(line=0, character=7), 69 | ) 70 | 71 | assert fix.range == expected_range 72 | assert fix.new_text == "1 + 1 = 2!" 73 | -------------------------------------------------------------------------------- /tests/e2e/test_completion.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest_asyncio 22 | from lsprotocol import types 23 | 24 | if typing.TYPE_CHECKING: 25 | from typing import Tuple 26 | 27 | from pygls.lsp.client import BaseLanguageClient 28 | 29 | 30 | @pytest_asyncio.fixture() 31 | async def json_server(get_client_for): 32 | async for result in get_client_for("json_server.py"): 33 | yield result 34 | 35 | 36 | async def test_completion( 37 | json_server: Tuple[BaseLanguageClient, types.InitializeResult], 38 | uri_for, 39 | ): 40 | """Ensure that the completion methods are working as expected.""" 41 | client, initialize_result = json_server 42 | 43 | completion_provider = initialize_result.capabilities.completion_provider 44 | assert completion_provider 45 | assert completion_provider.trigger_characters == [","] 46 | assert completion_provider.all_commit_characters == [":"] 47 | 48 | test_uri = uri_for("test.json") 49 | assert test_uri is not None 50 | 51 | response = await client.text_document_completion_async( 52 | types.CompletionParams( 53 | text_document=types.TextDocumentIdentifier(uri=test_uri), 54 | position=types.Position(line=0, character=0), 55 | ) 56 | ) 57 | 58 | labels = {i.label for i in response.items} 59 | assert labels == set(['"', "[", "]", "{", "}"]) 60 | -------------------------------------------------------------------------------- /tests/e2e/test_declaration.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest_asyncio 22 | from lsprotocol import types 23 | 24 | if typing.TYPE_CHECKING: 25 | from typing import Tuple 26 | 27 | from pygls.lsp.client import BaseLanguageClient 28 | 29 | 30 | @pytest_asyncio.fixture() 31 | async def goto(get_client_for): 32 | async for result in get_client_for("goto.py"): 33 | yield result 34 | 35 | 36 | async def test_declaration( 37 | goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for 38 | ): 39 | """Ensure that we can implement declaration requests.""" 40 | client, initialize_result = goto 41 | 42 | declaration_options = initialize_result.capabilities.declaration_provider 43 | assert declaration_options is True 44 | 45 | test_uri = uri_for("code.txt") 46 | test_path = path_for("code.txt") 47 | 48 | client.text_document_did_open( 49 | types.DidOpenTextDocumentParams( 50 | types.TextDocumentItem( 51 | uri=test_uri, 52 | language_id="plaintext", 53 | version=0, 54 | text=test_path.read_text(), 55 | ) 56 | ) 57 | ) 58 | 59 | response = await client.text_document_declaration_async( 60 | types.DeclarationParams( 61 | text_document=types.TextDocumentIdentifier(uri=test_uri), 62 | position=types.Position(line=6, character=47), 63 | ) 64 | ) 65 | assert response is None 66 | 67 | response = await client.text_document_declaration_async( 68 | types.DeclarationParams( 69 | text_document=types.TextDocumentIdentifier(uri=test_uri), 70 | position=types.Position(line=5, character=52), 71 | ) 72 | ) 73 | assert isinstance(response, types.Location) 74 | assert response.uri == test_uri 75 | assert response.range == types.Range( 76 | start=types.Position(line=5, character=10), 77 | end=types.Position(line=5, character=25), 78 | ) 79 | -------------------------------------------------------------------------------- /tests/e2e/test_definition.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest_asyncio 22 | from lsprotocol import types 23 | 24 | if typing.TYPE_CHECKING: 25 | from typing import Tuple 26 | 27 | from pygls.lsp.client import BaseLanguageClient 28 | 29 | 30 | @pytest_asyncio.fixture() 31 | async def goto(get_client_for): 32 | async for result in get_client_for("goto.py"): 33 | yield result 34 | 35 | 36 | async def test_definition( 37 | goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for 38 | ): 39 | """Ensure that we can implement type definition requests.""" 40 | client, initialize_result = goto 41 | 42 | definition_options = initialize_result.capabilities.definition_provider 43 | assert definition_options is True 44 | 45 | test_uri = uri_for("code.txt") 46 | test_path = path_for("code.txt") 47 | 48 | client.text_document_did_open( 49 | types.DidOpenTextDocumentParams( 50 | types.TextDocumentItem( 51 | uri=test_uri, 52 | language_id="plaintext", 53 | version=0, 54 | text=test_path.read_text(), 55 | ) 56 | ) 57 | ) 58 | 59 | response = await client.text_document_definition_async( 60 | types.DefinitionParams( 61 | text_document=types.TextDocumentIdentifier(uri=test_uri), 62 | position=types.Position(line=6, character=47), 63 | ) 64 | ) 65 | assert response is None 66 | 67 | response = await client.text_document_definition_async( 68 | types.DefinitionParams( 69 | text_document=types.TextDocumentIdentifier(uri=test_uri), 70 | position=types.Position(line=5, character=20), 71 | ) 72 | ) 73 | assert isinstance(response, types.Location) 74 | assert response.uri == test_uri 75 | assert response.range == types.Range( 76 | start=types.Position(line=0, character=5), 77 | end=types.Position(line=0, character=14), 78 | ) 79 | -------------------------------------------------------------------------------- /tests/e2e/test_hover.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest 22 | import pytest_asyncio 23 | from lsprotocol import types 24 | 25 | if typing.TYPE_CHECKING: 26 | from typing import List 27 | from typing import Tuple 28 | 29 | from pygls.lsp.client import BaseLanguageClient 30 | 31 | 32 | @pytest_asyncio.fixture(scope="module", loop_scope="module") 33 | async def hover(get_client_for): 34 | async for result in get_client_for("hover.py"): 35 | yield result 36 | 37 | 38 | @pytest.mark.parametrize( 39 | "position, expected", 40 | [ 41 | ( 42 | types.Position(line=0, character=3), 43 | "\n".join( 44 | [ 45 | "# Sat 01 Feb 2020", 46 | "", 47 | "| Format | Value |", 48 | "|:-|-:|", 49 | "| `%H:%M:%S` | 00:00:00 |", 50 | "| `%d/%m/%y` | 01/02/20 |", 51 | "| `%Y-%m-%d` | 2020-02-01 |", 52 | "| `%Y-%m-%dT%H:%M:%S` | 2020-02-01T00:00:00 |", 53 | ] 54 | ), 55 | ), 56 | (types.Position(line=1, character=3), None), 57 | ( 58 | types.Position(line=2, character=3), 59 | "\n".join( 60 | [ 61 | "# Sun 02 Jan 1921", 62 | "", 63 | "| Format | Value |", 64 | "|:-|-:|", 65 | "| `%H:%M:%S` | 23:59:00 |", 66 | "| `%d/%m/%y` | 02/01/21 |", 67 | "| `%Y-%m-%d` | 1921-01-02 |", 68 | "| `%Y-%m-%dT%H:%M:%S` | 1921-01-02T23:59:00 |", 69 | ] 70 | ), 71 | ), 72 | ], 73 | ) 74 | @pytest.mark.asyncio(loop_scope="module") 75 | async def test_hover( 76 | hover: Tuple[BaseLanguageClient, types.InitializeResult], 77 | uri_for, 78 | position: types.Position, 79 | expected: List[str], 80 | ): 81 | """Ensure that the example hover server is working as expected.""" 82 | client, initialize_result = hover 83 | 84 | hover_options = initialize_result.capabilities.hover_provider 85 | assert hover_options is True 86 | 87 | test_uri = uri_for("dates.txt") 88 | response = await client.text_document_hover_async( 89 | types.HoverParams( 90 | position=position, 91 | text_document=types.TextDocumentIdentifier(uri=test_uri), 92 | ) 93 | ) 94 | 95 | if expected is None: 96 | assert response is None 97 | return 98 | 99 | assert response == types.Hover( 100 | contents=types.MarkupContent( 101 | kind=types.MarkupKind.Markdown, 102 | value=expected, 103 | ), 104 | range=types.Range( 105 | start=types.Position(line=position.line, character=0), 106 | end=types.Position(line=position.line + 1, character=0), 107 | ), 108 | ) 109 | -------------------------------------------------------------------------------- /tests/e2e/test_implementation.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest_asyncio 22 | from lsprotocol import types 23 | 24 | if typing.TYPE_CHECKING: 25 | from typing import Tuple 26 | 27 | from pygls.lsp.client import BaseLanguageClient 28 | 29 | 30 | @pytest_asyncio.fixture() 31 | async def goto(get_client_for): 32 | async for result in get_client_for("goto.py"): 33 | yield result 34 | 35 | 36 | async def test_implementation( 37 | goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for 38 | ): 39 | """Ensure that we can implement type implementation requests.""" 40 | client, initialize_result = goto 41 | 42 | implementation_options = initialize_result.capabilities.implementation_provider 43 | assert implementation_options is True 44 | 45 | test_uri = uri_for("code.txt") 46 | test_path = path_for("code.txt") 47 | 48 | client.text_document_did_open( 49 | types.DidOpenTextDocumentParams( 50 | types.TextDocumentItem( 51 | uri=test_uri, 52 | language_id="plaintext", 53 | version=0, 54 | text=test_path.read_text(), 55 | ) 56 | ) 57 | ) 58 | 59 | response = await client.text_document_implementation_async( 60 | types.ImplementationParams( 61 | text_document=types.TextDocumentIdentifier(uri=test_uri), 62 | position=types.Position(line=6, character=47), 63 | ) 64 | ) 65 | assert response is None 66 | 67 | response = await client.text_document_implementation_async( 68 | types.ImplementationParams( 69 | text_document=types.TextDocumentIdentifier(uri=test_uri), 70 | position=types.Position(line=5, character=46), 71 | ) 72 | ) 73 | assert isinstance(response, types.Location) 74 | assert response.uri == test_uri 75 | assert response.range == types.Range( 76 | start=types.Position(line=3, character=3), 77 | end=types.Position(line=3, character=7), 78 | ) 79 | -------------------------------------------------------------------------------- /tests/e2e/test_inlay_hints.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest_asyncio 22 | from lsprotocol import types 23 | 24 | if typing.TYPE_CHECKING: 25 | from typing import Tuple 26 | 27 | from pygls.lsp.client import BaseLanguageClient 28 | 29 | 30 | @pytest_asyncio.fixture() 31 | async def inlay_hints(get_client_for): 32 | async for result in get_client_for("inlay_hints.py"): 33 | yield result 34 | 35 | 36 | async def test_inlay_hints( 37 | inlay_hints: Tuple[BaseLanguageClient, types.InitializeResult], uri_for 38 | ): 39 | """Ensure that the example code action server is working as expected.""" 40 | client, initialize_result = inlay_hints 41 | 42 | inlay_hint_provider = initialize_result.capabilities.inlay_hint_provider 43 | assert inlay_hint_provider.resolve_provider is True 44 | 45 | test_uri = uri_for("sums.txt") 46 | assert test_uri is not None 47 | 48 | response = await client.text_document_inlay_hint_async( 49 | types.InlayHintParams( 50 | text_document=types.TextDocumentIdentifier(uri=test_uri), 51 | range=types.Range( 52 | start=types.Position(line=3, character=0), 53 | end=types.Position(line=4, character=0), 54 | ), 55 | ) 56 | ) 57 | 58 | assert len(response) == 2 59 | two, three = response[0], response[1] 60 | 61 | assert two.label == ":10" 62 | assert two.tooltip is None 63 | 64 | assert three.label == ":11" 65 | assert three.tooltip is None 66 | 67 | resolved = await client.inlay_hint_resolve_async(three) 68 | assert resolved.tooltip == "Binary representation of the number: 3" 69 | -------------------------------------------------------------------------------- /tests/e2e/test_links.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest 22 | import pytest_asyncio 23 | from lsprotocol import types 24 | 25 | if typing.TYPE_CHECKING: 26 | from typing import Tuple 27 | 28 | from pygls.lsp.client import BaseLanguageClient 29 | 30 | 31 | @pytest_asyncio.fixture(scope="module", loop_scope="module") 32 | async def links(get_client_for): 33 | async for result in get_client_for("links.py"): 34 | yield result 35 | 36 | 37 | def range_from_str(range_: str) -> types.Range: 38 | start, end = range_.split("-") 39 | start_line, start_char = start.split(":") 40 | end_line, end_char = end.split(":") 41 | 42 | return types.Range( 43 | start=types.Position(line=int(start_line), character=int(start_char)), 44 | end=types.Position(line=int(end_line), character=int(end_char)), 45 | ) 46 | 47 | 48 | @pytest.mark.asyncio(loop_scope="module") 49 | async def test_document_link( 50 | links: Tuple[BaseLanguageClient, types.InitializeResult], uri_for 51 | ): 52 | """Ensure that the example links server is working as expected.""" 53 | client, initialize_result = links 54 | 55 | document_link_options = initialize_result.capabilities.document_link_provider 56 | assert document_link_options.resolve_provider is True 57 | 58 | test_uri = uri_for("links.txt") 59 | response = await client.text_document_document_link_async( 60 | types.DocumentLinkParams( 61 | text_document=types.TextDocumentIdentifier(uri=test_uri) 62 | ) 63 | ) 64 | 65 | assert response == [ 66 | types.DocumentLink( 67 | range=range_from_str("0:6-0:35"), 68 | data=dict(type="github", target="openlawlibrary/pygls"), 69 | ), 70 | types.DocumentLink( 71 | range=range_from_str("1:30-1:42"), 72 | data=dict(type="pypi", target="pygls"), 73 | ), 74 | types.DocumentLink( 75 | range=range_from_str("1:73-1:90"), 76 | data=dict(type="pypi", target="lsprotocol"), 77 | ), 78 | ] 79 | 80 | 81 | @pytest.mark.asyncio(loop_scope="module") 82 | async def test_document_link_resolve( 83 | links: Tuple[BaseLanguageClient, types.InitializeResult], uri_for 84 | ): 85 | """Ensure that the server can resolve document links correctly.""" 86 | 87 | client, _ = links 88 | link = types.DocumentLink( 89 | range=range_from_str("0:6-0:35"), 90 | data=dict(type="github", target="openlawlibrary/pygls"), 91 | ) 92 | 93 | response = await client.document_link_resolve_async(link) 94 | 95 | assert response == types.DocumentLink( 96 | range=range_from_str("0:6-0:35"), 97 | target="https://github.com/openlawlibrary/pygls", 98 | tooltip="Github - openlawlibrary/pygls", 99 | data=dict(type="github", target="openlawlibrary/pygls"), 100 | ) 101 | -------------------------------------------------------------------------------- /tests/e2e/test_references.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest_asyncio 22 | from lsprotocol import types 23 | 24 | if typing.TYPE_CHECKING: 25 | from typing import Tuple 26 | 27 | from pygls.lsp.client import BaseLanguageClient 28 | 29 | 30 | @pytest_asyncio.fixture() 31 | async def goto(get_client_for): 32 | async for result in get_client_for("goto.py"): 33 | yield result 34 | 35 | 36 | async def test_type_definition( 37 | goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for 38 | ): 39 | """Ensure that we can implement type definition requests.""" 40 | client, initialize_result = goto 41 | 42 | reference_options = initialize_result.capabilities.references_provider 43 | assert reference_options is True 44 | 45 | test_uri = uri_for("code.txt") 46 | test_path = path_for("code.txt") 47 | 48 | client.text_document_did_open( 49 | types.DidOpenTextDocumentParams( 50 | types.TextDocumentItem( 51 | uri=test_uri, 52 | language_id="plaintext", 53 | version=0, 54 | text=test_path.read_text(), 55 | ) 56 | ) 57 | ) 58 | 59 | response = await client.text_document_references_async( 60 | types.ReferenceParams( 61 | context=types.ReferenceContext(include_declaration=True), 62 | text_document=types.TextDocumentIdentifier(uri=test_uri), 63 | position=types.Position(line=0, character=0), 64 | ) 65 | ) 66 | assert response is None 67 | 68 | response = await client.text_document_references_async( 69 | types.ReferenceParams( 70 | context=types.ReferenceContext(include_declaration=True), 71 | text_document=types.TextDocumentIdentifier(uri=test_uri), 72 | position=types.Position(line=3, character=5), 73 | ) 74 | ) 75 | assert len(response) == 2 76 | 77 | assert response[0].uri == test_uri 78 | assert response[0].range == types.Range( 79 | start=types.Position(line=3, character=3), 80 | end=types.Position(line=3, character=7), 81 | ) 82 | 83 | assert response[1].uri == test_uri 84 | assert response[1].range == types.Range( 85 | start=types.Position(line=5, character=45), 86 | end=types.Position(line=5, character=49), 87 | ) 88 | -------------------------------------------------------------------------------- /tests/e2e/test_register_during_initialize.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest_asyncio 22 | from lsprotocol import types 23 | 24 | if typing.TYPE_CHECKING: 25 | from typing import Tuple 26 | 27 | from pygls.lsp.client import BaseLanguageClient 28 | 29 | 30 | @pytest_asyncio.fixture() 31 | async def without_formatting(get_client_for): 32 | async for result in get_client_for("register_during_initialize.py"): 33 | yield result 34 | 35 | 36 | async def test_without_formatting( 37 | without_formatting: Tuple[BaseLanguageClient, types.InitializeResult], uri_for 38 | ): 39 | """Ensure that the formatting provider is not set by default.""" 40 | client, initialize_result = without_formatting 41 | 42 | format_options = initialize_result.capabilities.document_formatting_provider 43 | assert format_options is None 44 | 45 | 46 | @pytest_asyncio.fixture() 47 | async def with_formatting(get_client_for): 48 | init_options = {"formatting": True} 49 | 50 | async for result in get_client_for( 51 | "register_during_initialize.py", initialization_options=init_options 52 | ): 53 | yield result 54 | 55 | 56 | async def test_with_formatting( 57 | with_formatting: Tuple[BaseLanguageClient, types.InitializeResult], uri_for 58 | ): 59 | """Ensure that the formatting provider is present when requested.""" 60 | client, initialize_result = with_formatting 61 | 62 | format_options = initialize_result.capabilities.document_formatting_provider 63 | assert format_options is not None 64 | -------------------------------------------------------------------------------- /tests/e2e/test_semantic_tokens.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest 22 | import pytest_asyncio 23 | from lsprotocol import types 24 | 25 | if typing.TYPE_CHECKING: 26 | from typing import List, Tuple 27 | 28 | from pygls.lsp.client import BaseLanguageClient 29 | 30 | 31 | @pytest_asyncio.fixture(scope="module", loop_scope="module") 32 | async def semantic_tokens(get_client_for): 33 | async for result in get_client_for("semantic_tokens.py"): 34 | yield result 35 | 36 | 37 | @pytest.mark.parametrize( 38 | "text,expected", 39 | [ 40 | # Just a handful of cases to check we've got the basics right 41 | ("fn", [0, 0, 2, 0, 0]), 42 | ("type", [0, 0, 4, 0, 0]), 43 | ("Rectangle", [0, 0, 9, 5, 0]), 44 | ( 45 | "type Rectangle", 46 | [ 47 | # fmt: off 48 | 0, 0, 4, 0, 0, 49 | 0, 5, 9, 5, 8, 50 | # fmt: on 51 | ], 52 | ), 53 | ( 54 | "fn area", 55 | [ 56 | # fmt: off 57 | 0, 0, 2, 0, 0, 58 | 0, 3, 4, 2, 8, 59 | # fmt: on 60 | ], 61 | ), 62 | ( 63 | "fn\n area", 64 | [ 65 | # fmt: off 66 | 0, 0, 2, 0, 0, 67 | 1, 1, 4, 2, 8, 68 | # fmt: on 69 | ], 70 | ), 71 | ], 72 | ) 73 | @pytest.mark.asyncio(loop_scope="module") 74 | async def test_semantic_tokens_full( 75 | semantic_tokens: Tuple[BaseLanguageClient, types.InitializeResult], 76 | uri_for, 77 | path_for, 78 | text: str, 79 | expected: List[int], 80 | ): 81 | """Ensure that the example semantic tokens server is working as expected.""" 82 | client, initialize_result = semantic_tokens 83 | 84 | semantic_tokens_options = initialize_result.capabilities.semantic_tokens_provider 85 | assert semantic_tokens_options.full is True 86 | 87 | legend = semantic_tokens_options.legend 88 | assert legend.token_types == [ 89 | "keyword", 90 | "variable", 91 | "function", 92 | "operator", 93 | "parameter", 94 | "type", 95 | ] 96 | assert legend.token_modifiers == [ 97 | "deprecated", 98 | "readonly", 99 | "defaultLibrary", 100 | "definition", 101 | ] 102 | 103 | test_uri = uri_for("code.txt") 104 | 105 | client.text_document_did_open( 106 | types.DidOpenTextDocumentParams( 107 | types.TextDocumentItem( 108 | uri=test_uri, 109 | language_id="plaintext", 110 | version=0, 111 | text=text, 112 | ) 113 | ) 114 | ) 115 | 116 | response = await client.text_document_semantic_tokens_full_async( 117 | types.SemanticTokensParams( 118 | text_document=types.TextDocumentIdentifier(uri=test_uri), 119 | ) 120 | ) 121 | assert response.data == expected 122 | -------------------------------------------------------------------------------- /tests/e2e/test_type_definition.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import pytest_asyncio 22 | from lsprotocol import types 23 | 24 | if typing.TYPE_CHECKING: 25 | from typing import Tuple 26 | 27 | from pygls.lsp.client import BaseLanguageClient 28 | 29 | 30 | @pytest_asyncio.fixture() 31 | async def goto(get_client_for): 32 | async for result in get_client_for("goto.py"): 33 | yield result 34 | 35 | 36 | async def test_type_definition( 37 | goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for 38 | ): 39 | """Ensure that we can implement type definition requests.""" 40 | client, initialize_result = goto 41 | 42 | type_definition_options = initialize_result.capabilities.type_definition_provider 43 | assert type_definition_options is True 44 | 45 | test_uri = uri_for("code.txt") 46 | test_path = path_for("code.txt") 47 | 48 | client.text_document_did_open( 49 | types.DidOpenTextDocumentParams( 50 | types.TextDocumentItem( 51 | uri=test_uri, 52 | language_id="plaintext", 53 | version=0, 54 | text=test_path.read_text(), 55 | ) 56 | ) 57 | ) 58 | 59 | response = await client.text_document_type_definition_async( 60 | types.TypeDefinitionParams( 61 | text_document=types.TextDocumentIdentifier(uri=test_uri), 62 | position=types.Position(line=6, character=47), 63 | ) 64 | ) 65 | assert response is None 66 | 67 | response = await client.text_document_type_definition_async( 68 | types.TypeDefinitionParams( 69 | text_document=types.TextDocumentIdentifier(uri=test_uri), 70 | position=types.Position(line=5, character=52), 71 | ) 72 | ) 73 | assert isinstance(response, types.Location) 74 | assert response.uri == test_uri 75 | assert response.range == types.Range( 76 | start=types.Position(line=0, character=5), 77 | end=types.Position(line=0, character=14), 78 | ) 79 | -------------------------------------------------------------------------------- /tests/ls_setup.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | import os 18 | import threading 19 | 20 | import pytest 21 | from lsprotocol.types import ( 22 | EXIT, 23 | INITIALIZE, 24 | SHUTDOWN, 25 | ClientCapabilities, 26 | InitializeParams, 27 | ) 28 | 29 | from pygls.lsp.server import LanguageServer 30 | 31 | from . import CMD_ASYNC, CMD_SYNC, CMD_THREAD 32 | from ._init_server_stall_fix_hack import retry_stalled_init_fix_hack 33 | 34 | CALL_TIMEOUT = 3 35 | 36 | 37 | def setup_ls_features(server): 38 | # Commands 39 | @server.command(CMD_ASYNC) 40 | async def cmd_test3(ls, *args): # pylint: disable=unused-variable 41 | return True, threading.get_ident() 42 | 43 | @server.thread() 44 | @server.command(CMD_THREAD) 45 | def cmd_test1(ls, *args): # pylint: disable=unused-variable 46 | return True, threading.get_ident() 47 | 48 | @server.command(CMD_SYNC) 49 | def cmd_test2(ls, *args): # pylint: disable=unused-variable 50 | return True, threading.get_ident() 51 | 52 | 53 | class ClientServer: 54 | def __init__(self, LS=LanguageServer): 55 | # Client to Server pipe 56 | csr, csw = os.pipe() 57 | # Server to client pipe 58 | scr, scw = os.pipe() 59 | 60 | # Setup Server 61 | self.server = LS("server", "v1") 62 | self.server_thread = threading.Thread( 63 | name="Server Thread", 64 | target=self.server.start_io, 65 | args=(os.fdopen(csr, "rb"), os.fdopen(scw, "wb")), 66 | ) 67 | self.server_thread.daemon = True 68 | 69 | # Setup client 70 | self.client = LS("client", "v1") 71 | self.client_thread = threading.Thread( 72 | name="Client Thread", 73 | target=self.client.start_io, 74 | args=(os.fdopen(scr, "rb"), os.fdopen(csw, "wb")), 75 | ) 76 | self.client_thread.daemon = True 77 | 78 | @classmethod 79 | def decorate(cls): 80 | return pytest.mark.parametrize("client_server", [cls], indirect=True) 81 | 82 | def start(self): 83 | self.server_thread.start() 84 | self.server.thread_id = self.server_thread.ident 85 | self.client_thread.start() 86 | self.initialize() 87 | 88 | def stop(self): 89 | shutdown_response = self.client.protocol.send_request(SHUTDOWN).result() 90 | assert shutdown_response is None 91 | self.client.protocol.notify(EXIT) 92 | self.server_thread.join() 93 | self.client._stop_event.set() 94 | 95 | self.client_thread.join() 96 | 97 | @retry_stalled_init_fix_hack() 98 | def initialize(self): 99 | timeout = None if "DISABLE_TIMEOUT" in os.environ else 1 100 | response = self.client.protocol.send_request( 101 | INITIALIZE, 102 | InitializeParams( 103 | process_id=12345, root_uri="file://", capabilities=ClientCapabilities() 104 | ), 105 | ).result(timeout=timeout) 106 | assert response.capabilities is not None 107 | 108 | def __iter__(self): 109 | yield self.client 110 | yield self.server 111 | -------------------------------------------------------------------------------- /tests/lsp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlawlibrary/pygls/34f646f8441ab9fab825ce61ab3f25771d15a88f/tests/lsp/__init__.py -------------------------------------------------------------------------------- /tests/lsp/test_document_highlight.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | 18 | from typing import List, Optional 19 | 20 | from lsprotocol.types import TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT 21 | from lsprotocol.types import ( 22 | DocumentHighlight, 23 | DocumentHighlightKind, 24 | DocumentHighlightOptions, 25 | DocumentHighlightParams, 26 | Position, 27 | Range, 28 | TextDocumentIdentifier, 29 | ) 30 | 31 | from ..conftest import ClientServer 32 | 33 | 34 | class ConfiguredLS(ClientServer): 35 | def __init__(self): 36 | super().__init__() 37 | 38 | @self.server.feature( 39 | TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT, 40 | DocumentHighlightOptions(), 41 | ) 42 | def f(params: DocumentHighlightParams) -> Optional[List[DocumentHighlight]]: 43 | if params.text_document.uri == "file://return.list": 44 | return [ 45 | DocumentHighlight( 46 | range=Range( 47 | start=Position(line=0, character=0), 48 | end=Position(line=1, character=1), 49 | ), 50 | ), 51 | DocumentHighlight( 52 | range=Range( 53 | start=Position(line=1, character=1), 54 | end=Position(line=2, character=2), 55 | ), 56 | kind=DocumentHighlightKind.Write, 57 | ), 58 | ] 59 | else: 60 | return None 61 | 62 | 63 | @ConfiguredLS.decorate() 64 | def test_capabilities(client_server): 65 | _, server = client_server 66 | capabilities = server.server_capabilities 67 | 68 | assert capabilities.document_highlight_provider 69 | 70 | 71 | @ConfiguredLS.decorate() 72 | def test_document_highlight_return_list(client_server): 73 | client, _ = client_server 74 | response = client.protocol.send_request( 75 | TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT, 76 | DocumentHighlightParams( 77 | text_document=TextDocumentIdentifier(uri="file://return.list"), 78 | position=Position(line=0, character=0), 79 | ), 80 | ).result() 81 | 82 | assert response 83 | 84 | assert response[0].range.start.line == 0 85 | assert response[0].range.start.character == 0 86 | assert response[0].range.end.line == 1 87 | assert response[0].range.end.character == 1 88 | assert response[0].kind is None 89 | 90 | assert response[1].range.start.line == 1 91 | assert response[1].range.start.character == 1 92 | assert response[1].range.end.line == 2 93 | assert response[1].range.end.character == 2 94 | assert response[1].kind == DocumentHighlightKind.Write 95 | 96 | 97 | @ConfiguredLS.decorate() 98 | def test_document_highlight_return_none(client_server): 99 | client, _ = client_server 100 | response = client.protocol.send_request( 101 | TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT, 102 | DocumentHighlightParams( 103 | text_document=TextDocumentIdentifier(uri="file://return.none"), 104 | position=Position(line=0, character=0), 105 | ), 106 | ).result() 107 | 108 | assert response is None 109 | -------------------------------------------------------------------------------- /tests/lsp/test_folding_range.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | 18 | from typing import List, Optional 19 | 20 | from lsprotocol.types import TEXT_DOCUMENT_FOLDING_RANGE 21 | from lsprotocol.types import ( 22 | FoldingRange, 23 | FoldingRangeKind, 24 | FoldingRangeOptions, 25 | FoldingRangeParams, 26 | TextDocumentIdentifier, 27 | ) 28 | 29 | from ..conftest import ClientServer 30 | 31 | 32 | class ConfiguredLS(ClientServer): 33 | def __init__(self): 34 | super().__init__() 35 | 36 | @self.server.feature( 37 | TEXT_DOCUMENT_FOLDING_RANGE, 38 | FoldingRangeOptions(), 39 | ) 40 | def f(params: FoldingRangeParams) -> Optional[List[FoldingRange]]: 41 | if params.text_document.uri == "file://return.list": 42 | return [ 43 | FoldingRange( 44 | start_line=0, 45 | end_line=0, 46 | start_character=1, 47 | end_character=1, 48 | kind=FoldingRangeKind.Comment, 49 | ), 50 | ] 51 | else: 52 | return None 53 | 54 | 55 | @ConfiguredLS.decorate() 56 | def test_capabilities(client_server): 57 | _, server = client_server 58 | capabilities = server.server_capabilities 59 | 60 | assert capabilities.folding_range_provider 61 | 62 | 63 | @ConfiguredLS.decorate() 64 | def test_folding_range_return_list(client_server): 65 | client, _ = client_server 66 | response = client.protocol.send_request( 67 | TEXT_DOCUMENT_FOLDING_RANGE, 68 | FoldingRangeParams( 69 | text_document=TextDocumentIdentifier(uri="file://return.list"), 70 | ), 71 | ).result() 72 | 73 | assert response 74 | 75 | assert response[0].start_line == 0 76 | assert response[0].end_line == 0 77 | assert response[0].start_character == 1 78 | assert response[0].end_character == 1 79 | assert response[0].kind == FoldingRangeKind.Comment 80 | 81 | 82 | @ConfiguredLS.decorate() 83 | def test_folding_range_return_none(client_server): 84 | client, _ = client_server 85 | response = client.protocol.send_request( 86 | TEXT_DOCUMENT_FOLDING_RANGE, 87 | FoldingRangeParams( 88 | text_document=TextDocumentIdentifier(uri="file://return.none"), 89 | ), 90 | ).result() 91 | 92 | assert response is None 93 | -------------------------------------------------------------------------------- /tests/lsp/test_linked_editing_range.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | 18 | from typing import Optional 19 | 20 | from lsprotocol.types import TEXT_DOCUMENT_LINKED_EDITING_RANGE 21 | from lsprotocol.types import ( 22 | LinkedEditingRangeOptions, 23 | LinkedEditingRangeParams, 24 | LinkedEditingRanges, 25 | Position, 26 | Range, 27 | TextDocumentIdentifier, 28 | ) 29 | 30 | from ..conftest import ClientServer 31 | 32 | 33 | class ConfiguredLS(ClientServer): 34 | def __init__(self): 35 | super().__init__() 36 | 37 | @self.server.feature( 38 | TEXT_DOCUMENT_LINKED_EDITING_RANGE, 39 | LinkedEditingRangeOptions(), 40 | ) 41 | def f(params: LinkedEditingRangeParams) -> Optional[LinkedEditingRanges]: 42 | if params.text_document.uri == "file://return.ranges": 43 | return LinkedEditingRanges( 44 | ranges=[ 45 | Range( 46 | start=Position(line=0, character=0), 47 | end=Position(line=1, character=1), 48 | ), 49 | Range( 50 | start=Position(line=1, character=1), 51 | end=Position(line=2, character=2), 52 | ), 53 | ], 54 | word_pattern="pattern", 55 | ) 56 | else: 57 | return None 58 | 59 | 60 | @ConfiguredLS.decorate() 61 | def test_capabilities(client_server): 62 | _, server = client_server 63 | capabilities = server.server_capabilities 64 | 65 | assert capabilities.linked_editing_range_provider 66 | 67 | 68 | @ConfiguredLS.decorate() 69 | def test_linked_editing_ranges_return_ranges(client_server): 70 | client, _ = client_server 71 | response = client.protocol.send_request( 72 | TEXT_DOCUMENT_LINKED_EDITING_RANGE, 73 | LinkedEditingRangeParams( 74 | text_document=TextDocumentIdentifier(uri="file://return.ranges"), 75 | position=Position(line=0, character=0), 76 | ), 77 | ).result() 78 | 79 | assert response 80 | 81 | assert response.ranges[0].start.line == 0 82 | assert response.ranges[0].start.character == 0 83 | assert response.ranges[0].end.line == 1 84 | assert response.ranges[0].end.character == 1 85 | assert response.ranges[1].start.line == 1 86 | assert response.ranges[1].start.character == 1 87 | assert response.ranges[1].end.line == 2 88 | assert response.ranges[1].end.character == 2 89 | assert response.word_pattern == "pattern" 90 | 91 | 92 | @ConfiguredLS.decorate() 93 | def test_linked_editing_ranges_return_none(client_server): 94 | client, _ = client_server 95 | response = client.protocol.send_request( 96 | TEXT_DOCUMENT_LINKED_EDITING_RANGE, 97 | LinkedEditingRangeParams( 98 | text_document=TextDocumentIdentifier(uri="file://return.none"), 99 | position=Position(line=0, character=0), 100 | ), 101 | ).result() 102 | 103 | assert response is None 104 | -------------------------------------------------------------------------------- /tests/lsp/test_moniker.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | 18 | from typing import List, Optional 19 | 20 | from lsprotocol.types import TEXT_DOCUMENT_MONIKER 21 | from lsprotocol.types import ( 22 | Moniker, 23 | MonikerKind, 24 | MonikerOptions, 25 | MonikerParams, 26 | Position, 27 | TextDocumentIdentifier, 28 | UniquenessLevel, 29 | ) 30 | 31 | from ..conftest import ClientServer 32 | 33 | 34 | class ConfiguredLS(ClientServer): 35 | def __init__(self): 36 | super().__init__() 37 | 38 | @self.server.feature( 39 | TEXT_DOCUMENT_MONIKER, 40 | MonikerOptions(), 41 | ) 42 | def f(params: MonikerParams) -> Optional[List[Moniker]]: 43 | if params.text_document.uri == "file://return.list": 44 | return [ 45 | Moniker( 46 | scheme="test_scheme", 47 | identifier="test_identifier", 48 | unique=UniquenessLevel.Global, 49 | kind=MonikerKind.Local, 50 | ), 51 | ] 52 | else: 53 | return None 54 | 55 | 56 | @ConfiguredLS.decorate() 57 | def test_capabilities(client_server): 58 | _, server = client_server 59 | capabilities = server.server_capabilities 60 | 61 | assert capabilities.moniker_provider 62 | 63 | 64 | @ConfiguredLS.decorate() 65 | def test_moniker_return_list(client_server): 66 | client, _ = client_server 67 | response = client.protocol.send_request( 68 | TEXT_DOCUMENT_MONIKER, 69 | MonikerParams( 70 | text_document=TextDocumentIdentifier(uri="file://return.list"), 71 | position=Position(line=0, character=0), 72 | ), 73 | ).result() 74 | 75 | assert response 76 | 77 | assert response[0].scheme == "test_scheme" 78 | assert response[0].identifier == "test_identifier" 79 | assert response[0].unique == UniquenessLevel.Global 80 | assert response[0].kind == MonikerKind.Local 81 | 82 | 83 | @ConfiguredLS.decorate() 84 | def test_references_return_none(client_server): 85 | client, _ = client_server 86 | response = client.protocol.send_request( 87 | TEXT_DOCUMENT_MONIKER, 88 | MonikerParams( 89 | text_document=TextDocumentIdentifier(uri="file://return.none"), 90 | position=Position(line=0, character=0), 91 | ), 92 | ).result() 93 | 94 | assert response is None 95 | -------------------------------------------------------------------------------- /tests/lsp/test_selection_range.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | from typing import List, Optional 18 | 19 | from lsprotocol.types import TEXT_DOCUMENT_SELECTION_RANGE 20 | from lsprotocol.types import ( 21 | Position, 22 | Range, 23 | SelectionRange, 24 | SelectionRangeOptions, 25 | SelectionRangeParams, 26 | TextDocumentIdentifier, 27 | ) 28 | 29 | from ..conftest import ClientServer 30 | 31 | 32 | class ConfiguredLS(ClientServer): 33 | def __init__(self): 34 | super().__init__() 35 | 36 | @self.server.feature( 37 | TEXT_DOCUMENT_SELECTION_RANGE, 38 | SelectionRangeOptions(), 39 | ) 40 | def f(params: SelectionRangeParams) -> Optional[List[SelectionRange]]: 41 | if params.text_document.uri == "file://return.list": 42 | root = SelectionRange( 43 | range=Range( 44 | start=Position(line=0, character=0), 45 | end=Position(line=10, character=10), 46 | ), 47 | ) 48 | 49 | inner_range = SelectionRange( 50 | range=Range( 51 | start=Position(line=0, character=0), 52 | end=Position(line=1, character=1), 53 | ), 54 | parent=root, 55 | ) 56 | 57 | return [root, inner_range] 58 | else: 59 | return None 60 | 61 | 62 | @ConfiguredLS.decorate() 63 | def test_capabilities(client_server): 64 | _, server = client_server 65 | capabilities = server.server_capabilities 66 | 67 | assert capabilities.selection_range_provider 68 | 69 | 70 | @ConfiguredLS.decorate() 71 | def test_selection_range_return_list(client_server): 72 | client, _ = client_server 73 | response = client.protocol.send_request( 74 | TEXT_DOCUMENT_SELECTION_RANGE, 75 | SelectionRangeParams( 76 | # query="query", 77 | text_document=TextDocumentIdentifier(uri="file://return.list"), 78 | positions=[Position(line=0, character=0)], 79 | ), 80 | ).result() 81 | 82 | assert response 83 | 84 | root = response[0] 85 | assert root.range.start.line == 0 86 | assert root.range.start.character == 0 87 | assert root.range.end.line == 10 88 | assert root.range.end.character == 10 89 | assert root.parent is None 90 | 91 | assert response[1].range.start.line == 0 92 | assert response[1].range.start.character == 0 93 | assert response[1].range.end.line == 1 94 | assert response[1].range.end.character == 1 95 | assert response[1].parent == root 96 | 97 | 98 | @ConfiguredLS.decorate() 99 | def test_selection_range_return_none(client_server): 100 | client, _ = client_server 101 | response = client.protocol.send_request( 102 | TEXT_DOCUMENT_SELECTION_RANGE, 103 | SelectionRangeParams( 104 | # query="query", 105 | text_document=TextDocumentIdentifier(uri="file://return.none"), 106 | positions=[Position(line=0, character=0)], 107 | ), 108 | ).result() 109 | 110 | assert response is None 111 | -------------------------------------------------------------------------------- /tests/pyodide/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /tests/pyodide/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pyodide_tests", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "pyodide_tests", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "pyodide": "^0.27" 12 | } 13 | }, 14 | "node_modules/pyodide": { 15 | "version": "0.27.6", 16 | "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.27.6.tgz", 17 | "integrity": "sha512-ahiSHHs6iFKl2f8aO1wALINAlMNDLAtb44xCI87GQyH2tLDk8F8VWip3u1ZNIyglGSCYAOSFzWKwS1f9gBFVdg==", 18 | "license": "Apache-2.0", 19 | "dependencies": { 20 | "ws": "^8.5.0" 21 | }, 22 | "engines": { 23 | "node": ">=18.0.0" 24 | } 25 | }, 26 | "node_modules/ws": { 27 | "version": "8.18.0", 28 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 29 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 30 | "license": "MIT", 31 | "engines": { 32 | "node": ">=10.0.0" 33 | }, 34 | "peerDependencies": { 35 | "bufferutil": "^4.0.1", 36 | "utf-8-validate": ">=5.0.2" 37 | }, 38 | "peerDependenciesMeta": { 39 | "bufferutil": { 40 | "optional": true 41 | }, 42 | "utf-8-validate": { 43 | "optional": true 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/pyodide/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pyodide_tests", 3 | "version": "0.0.0", 4 | "description": "Simple wrapper that executes pygls servers in Pyodide", 5 | "main": "run_server.js", 6 | "author": "openlawlibrary", 7 | "dependencies": { 8 | "pyodide": "^0.27" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/pyodide/run_server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path') 3 | const { loadPyodide } = require('pyodide'); 4 | 5 | const consoleLog = console.log 6 | 7 | const WORKSPACE = path.join(__dirname, "..", "..", "examples", "servers", "workspace") 8 | const DIST = path.join(__dirname, "..", "..", "dist") 9 | 10 | // Create a file to log pyodide output to. 11 | const logFile = fs.createWriteStream("pyodide.log") 12 | 13 | function writeToFile(...args) { 14 | logFile.write(args[0] + `\n`); 15 | } 16 | 17 | // Load the workspace into the pyodide runtime. 18 | // 19 | // Unlike WASI, there is no "just works" solution for exposing the workspace/ folder 20 | // to the runtime - it's up to us to manually copy it into pyodide's in-memory filesystem. 21 | function loadWorkspace(pyodide) { 22 | const FS = pyodide.FS 23 | 24 | // Create a folder for the workspace to be copied into. 25 | FS.mkdir('/workspace') 26 | 27 | const workspace = fs.readdirSync(WORKSPACE) 28 | workspace.forEach((file) => { 29 | try { 30 | const filename = "/" + path.join("workspace", file) 31 | // consoleLog(`${file} -> ${filename}`) 32 | 33 | const stream = FS.open(filename, 'w+') 34 | const data = fs.readFileSync(path.join(WORKSPACE, file)) 35 | 36 | FS.write(stream, data, 0, data.length, 0) 37 | FS.close(stream) 38 | } catch (err) { 39 | consoleLog(err) 40 | } 41 | }) 42 | } 43 | 44 | // Find the *.whl file containing the build of pygls to test. 45 | function findWhl() { 46 | const files = fs.readdirSync(DIST); 47 | const whlFile = files.find(file => /pygls-.*\.whl/.test(file)); 48 | 49 | if (whlFile) { 50 | return path.join(DIST, whlFile); 51 | } else { 52 | consoleLog("Unable to find whl file.") 53 | throw new Error("Unable to find whl file."); 54 | } 55 | } 56 | 57 | async function runServer(serverCode) { 58 | // Annoyingly, while we can redirect stderr/stdout to a file during this setup stage 59 | // it doesn't prevent `micropip.install` from indirectly writing to console.log. 60 | // 61 | // Internally, `micropip.install` calls `pyodide.loadPackage` and doesn't expose loadPacakge's 62 | // options for redirecting output i.e. messageCallback. 63 | // 64 | // So instead, we override console.log globally. 65 | console.log = writeToFile 66 | const pyodide = await loadPyodide({ 67 | // stdin: 68 | stderr: writeToFile, 69 | }) 70 | 71 | loadWorkspace(pyodide) 72 | 73 | await pyodide.loadPackage("micropip") 74 | const micropip = pyodide.pyimport("micropip") 75 | await micropip.install(`file://${findWhl()}`) 76 | 77 | // Restore the original console.log 78 | console.log = consoleLog 79 | await pyodide.runPythonAsync(serverCode) 80 | } 81 | 82 | if (process.argv.length < 3) { 83 | console.error("Missing server.py file") 84 | process.exit(1) 85 | } 86 | 87 | 88 | const serverCode = fs.readFileSync(process.argv[2], 'utf8') 89 | 90 | logFile.once('open', (fd) => { 91 | runServer(serverCode).then(() => { 92 | logFile.end(); 93 | process.exit(0) 94 | }).catch(err => { 95 | logFile.write(`Error in server process\n${err}`) 96 | logFile.end(); 97 | process.exit(1); 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /tests/servers/invalid_json.py: -------------------------------------------------------------------------------- 1 | """This server does nothing but print invalid JSON.""" 2 | 3 | import sys 4 | import threading 5 | 6 | from pygls.io_ import run 7 | from pygls.protocol import JsonRPCProtocol, default_converter 8 | 9 | 10 | class InvalidJsonProtocol(JsonRPCProtocol): 11 | """A protocol that only sends messages containing invalid JSON.""" 12 | 13 | def handle_message(self, message): 14 | content = 'Content-Length: 5\r\n\r\n{"ll}'.encode("utf8") 15 | sys.stdout.buffer.write(content) 16 | sys.stdout.flush() 17 | 18 | 19 | def main(): 20 | run( 21 | threading.Event(), 22 | sys.stdin.buffer, 23 | InvalidJsonProtocol(None, default_converter()), 24 | ) 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /tests/servers/large_response.py: -------------------------------------------------------------------------------- 1 | """This server returns a particuarly large response.""" 2 | 3 | import sys 4 | 5 | from pygls.server import JsonRPCServer 6 | from pygls.protocol import JsonRPCProtocol, default_converter 7 | 8 | server = JsonRPCServer(JsonRPCProtocol, default_converter) 9 | 10 | 11 | @server.feature("get/numbers") 12 | def get_numbers(*args): 13 | return dict(numbers=list(range(100_000))) 14 | 15 | 16 | @server.feature("exit") 17 | def exit(*args): 18 | sys.exit(0) 19 | 20 | 21 | if __name__ == "__main__": 22 | server.start_io() 23 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright(c) Open Law Library. All rights reserved. # 3 | # See ThirdPartyNotices.txt in the project root for additional notices. # 4 | # # 5 | # Licensed under the Apache License, Version 2.0 (the "License") # 6 | # you may not use this file except in compliance with the License. # 7 | # You may obtain a copy of the License at # 8 | # # 9 | # http: // www.apache.org/licenses/LICENSE-2.0 # 10 | # # 11 | # Unless required by applicable law or agreed to in writing, software # 12 | # distributed under the License is distributed on an "AS IS" BASIS, # 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 14 | # See the License for the specific language governing permissions and # 15 | # limitations under the License. # 16 | ############################################################################ 17 | import asyncio 18 | import pathlib 19 | import sys 20 | from typing import Union 21 | 22 | import pytest 23 | from pygls import IS_PYODIDE 24 | 25 | from pygls.client import JsonRPCClient 26 | from pygls.exceptions import JsonRpcException, PyglsError 27 | 28 | 29 | SERVERS = pathlib.Path(__file__).parent / "servers" 30 | 31 | 32 | @pytest.mark.asyncio 33 | @pytest.mark.skipif(IS_PYODIDE, reason="Subprocesses are not available on pyodide.") 34 | async def test_client_detect_server_exit(): 35 | """Ensure that the client detects when the server process exits.""" 36 | 37 | class TestClient(JsonRPCClient): 38 | server_exit_called = False 39 | 40 | async def server_exit(self, server: asyncio.subprocess.Process): 41 | self.server_exit_called = True 42 | assert server.returncode == 0 43 | 44 | client = TestClient() 45 | await client.start_io(sys.executable, "-c", "print('Hello, World!')") 46 | await asyncio.sleep(1) 47 | await client.stop() 48 | 49 | message = "Expected the `server_exit` method to have been called." 50 | assert client.server_exit_called, message 51 | 52 | 53 | @pytest.mark.asyncio 54 | @pytest.mark.skipif(IS_PYODIDE, reason="Subprocesses are not available on pyodide.") 55 | async def test_client_detect_invalid_json(): 56 | """Ensure that the client can detect the case where the server returns invalid 57 | json.""" 58 | 59 | class TestClient(JsonRPCClient): 60 | report_error_called = False 61 | future = None 62 | 63 | def report_server_error( 64 | self, error: Exception, source: Union[PyglsError, JsonRpcException] 65 | ): 66 | self.report_error_called = True 67 | self.future.cancel() 68 | 69 | self._server.kill() 70 | self._stop_event.set() 71 | 72 | assert "Unterminated string" in str(error) 73 | 74 | client = TestClient() 75 | await client.start_io(sys.executable, str(SERVERS / "invalid_json.py")) 76 | 77 | future = client.protocol.send_request_async("method/name", {}) 78 | client.future = future 79 | 80 | try: 81 | await future 82 | except asyncio.CancelledError: 83 | pass # Ignore the exception generated by cancelling the future 84 | finally: 85 | await client.stop() 86 | 87 | assert_message = "Expected `report_server_error` to have been called" 88 | assert client.report_error_called, assert_message 89 | 90 | 91 | @pytest.mark.asyncio 92 | @pytest.mark.skipif(IS_PYODIDE, reason="Subprocesses are not available on pyodide.") 93 | async def test_client_large_responses(): 94 | """Ensure that the client can correctly handle large responses from a server.""" 95 | 96 | client = JsonRPCClient() 97 | await client.start_io(sys.executable, str(SERVERS / "large_response.py")) 98 | 99 | result = await client.protocol.send_request_async("get/numbers", {}, msg_id=1) 100 | assert len(result.numbers) == 100_000 101 | 102 | client.protocol.notify("exit", {}) 103 | await asyncio.wait_for(client.stop(), timeout=5.0) 104 | -------------------------------------------------------------------------------- /tests/test_server_connection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | from threading import Thread 5 | from unittest.mock import Mock 6 | 7 | import pytest 8 | 9 | from pygls import IS_PYODIDE 10 | from pygls.lsp.server import LanguageServer 11 | 12 | try: 13 | from websockets.asyncio.client import connect 14 | 15 | WEBSOCKETS_AVAILABLE = True 16 | except ImportError: 17 | WEBSOCKETS_AVAILABLE = False 18 | 19 | 20 | @pytest.mark.asyncio 21 | @pytest.mark.skipif(IS_PYODIDE, reason="threads are not available in pyodide.") 22 | async def test_tcp_connection_lost(): 23 | server = LanguageServer("pygls-test", "v1") 24 | 25 | server.protocol.set_writer = Mock() 26 | 27 | # Run the server over TCP in a separate thread 28 | server_thread = Thread( 29 | target=server.start_tcp, 30 | args=( 31 | "127.0.0.1", 32 | 0, 33 | ), 34 | ) 35 | server_thread.daemon = True 36 | server_thread.start() 37 | 38 | # Wait for server to be ready 39 | while server._server is None: 40 | await asyncio.sleep(0.5) 41 | 42 | # Simulate client's connection 43 | port = server._server.sockets[0].getsockname()[1] 44 | _, writer = await asyncio.open_connection("127.0.0.1", port) 45 | await asyncio.sleep(1) 46 | 47 | assert server.protocol.set_writer.called 48 | 49 | # Socket is closed (client's process is terminated) 50 | writer.close() 51 | await writer.wait_closed() 52 | 53 | # Give the server chance to shutdown. 54 | await asyncio.sleep(1) 55 | assert server._stop_event.is_set() 56 | 57 | 58 | @pytest.mark.asyncio 59 | @pytest.mark.skipif(IS_PYODIDE, reason="threads are not available in pyodide.") 60 | async def test_io_connection_lost(): 61 | # Client to Server pipe. 62 | csr, csw = os.pipe() 63 | # Server to client pipe. 64 | scr, scw = os.pipe() 65 | 66 | server = LanguageServer("pygls-test", "v1") 67 | server.protocol.set_writer = Mock() 68 | server_thread = Thread( 69 | target=server.start_io, args=(os.fdopen(csr, "rb"), os.fdopen(scw, "wb")) 70 | ) 71 | server_thread.daemon = True 72 | server_thread.start() 73 | 74 | # Wait for server to be ready 75 | while not server.protocol.set_writer.called: 76 | await asyncio.sleep(0.5) 77 | 78 | # Pipe is closed (client's process is terminated) 79 | os.close(csw) 80 | server_thread.join() 81 | 82 | 83 | @pytest.mark.asyncio 84 | @pytest.mark.skipif( 85 | IS_PYODIDE or not WEBSOCKETS_AVAILABLE, 86 | reason="threads are not available in pyodide", 87 | ) 88 | async def test_ws_server(): 89 | """Smoke test to ensure we can send/receive messages over websockets""" 90 | 91 | server = LanguageServer("pygls-test", "v1") 92 | 93 | # Run the server over Websockets in a separate thread 94 | server_thread = Thread( 95 | target=server.start_ws, 96 | args=( 97 | "127.0.0.1", 98 | 0, 99 | ), 100 | ) 101 | server_thread.daemon = True 102 | server_thread.start() 103 | 104 | # Wait for server to be ready 105 | while server._server is None: 106 | await asyncio.sleep(0.5) 107 | 108 | port = list(server._server.sockets)[0].getsockname()[1] 109 | # Simulate client's connection 110 | async with connect(f"ws://127.0.0.1:{port}") as connection: 111 | # Send an 'initialize' request 112 | msg = dict( 113 | jsonrpc="2.0", id=1, method="initialize", params=dict(capabilities=dict()) 114 | ) 115 | await connection.send(json.dumps(msg)) 116 | 117 | response = await connection.recv(decode=False) 118 | assert "result" in response.decode("utf8") 119 | 120 | # Shut the server down 121 | msg = dict( 122 | jsonrpc="2.0", id=2, method="shutdown", params=dict(capabilities=dict()) 123 | ) 124 | await connection.send(json.dumps(msg)) 125 | 126 | response = await connection.recv(decode=False) 127 | assert "result" in response.decode("utf8") 128 | 129 | # Finally, tell it to exit 130 | msg = dict(jsonrpc="2.0", id=2, method="exit", params=None) 131 | await connection.send(json.dumps(msg)) 132 | 133 | server_thread.join() 134 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Original work Copyright 2018 Palantir Technologies, Inc. # 3 | # Original work licensed under the MIT License. # 4 | # See ThirdPartyNotices.txt in the project root for license information. # 5 | # All modifications Copyright (c) Open Law Library. All rights reserved. # 6 | # # 7 | # Licensed under the Apache License, Version 2.0 (the "License") # 8 | # you may not use this file except in compliance with the License. # 9 | # You may obtain a copy of the License at # 10 | # # 11 | # http: // www.apache.org/licenses/LICENSE-2.0 # 12 | # # 13 | # Unless required by applicable law or agreed to in writing, software # 14 | # distributed under the License is distributed on an "AS IS" BASIS, # 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 16 | # See the License for the specific language governing permissions and # 17 | # limitations under the License. # 18 | ############################################################################ 19 | from lsprotocol.types import Location, Position, Range 20 | 21 | 22 | def test_position(): 23 | assert Position(line=1, character=2) == Position(line=1, character=2) 24 | assert Position(line=1, character=2) != Position(line=2, character=2) 25 | assert Position(line=1, character=2) <= Position(line=2, character=2) 26 | assert Position(line=2, character=2) >= Position(line=2, character=0) 27 | assert Position(line=1, character=2) != "something else" 28 | assert "1:2" == repr(Position(line=1, character=2)) 29 | 30 | 31 | def test_range(): 32 | assert Range( 33 | start=Position(line=1, character=2), end=Position(line=3, character=4) 34 | ) == Range(start=Position(line=1, character=2), end=Position(line=3, character=4)) 35 | assert Range( 36 | start=Position(line=0, character=2), end=Position(line=3, character=4) 37 | ) != Range(start=Position(line=1, character=2), end=Position(line=3, character=4)) 38 | assert ( 39 | Range(start=Position(line=0, character=2), end=Position(line=3, character=4)) 40 | != "something else" 41 | ) 42 | assert "1:2-3:4" == repr( 43 | Range(start=Position(line=1, character=2), end=Position(line=3, character=4)) 44 | ) 45 | 46 | 47 | def test_location(): 48 | assert Location( 49 | uri="file:///document.txt", 50 | range=Range( 51 | start=Position(line=1, character=2), end=Position(line=3, character=4) 52 | ), 53 | ) == Location( 54 | uri="file:///document.txt", 55 | range=Range( 56 | start=Position(line=1, character=2), end=Position(line=3, character=4) 57 | ), 58 | ) 59 | assert Location( 60 | uri="file:///document.txt", 61 | range=Range( 62 | start=Position(line=1, character=2), end=Position(line=3, character=4) 63 | ), 64 | ) != Location( 65 | uri="file:///another.txt", 66 | range=Range( 67 | start=Position(line=1, character=2), end=Position(line=3, character=4) 68 | ), 69 | ) 70 | assert ( 71 | Location( 72 | uri="file:///document.txt", 73 | range=Range( 74 | start=Position(line=1, character=2), end=Position(line=3, character=4) 75 | ), 76 | ) 77 | != "something else" 78 | ) 79 | assert "file:///document.txt:1:2-3:4" == repr( 80 | Location( 81 | uri="file:///document.txt", 82 | range=Range( 83 | start=Position(line=1, character=2), end=Position(line=3, character=4) 84 | ), 85 | ) 86 | ) 87 | -------------------------------------------------------------------------------- /tests/test_uris.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Original work Copyright 2018 Palantir Technologies, Inc. # 3 | # Original work licensed under the MIT License. # 4 | # See ThirdPartyNotices.txt in the project root for license information. # 5 | # All modifications Copyright (c) Open Law Library. All rights reserved. # 6 | # # 7 | # Licensed under the Apache License, Version 2.0 (the "License") # 8 | # you may not use this file except in compliance with the License. # 9 | # You may obtain a copy of the License at # 10 | # # 11 | # http: // www.apache.org/licenses/LICENSE-2.0 # 12 | # # 13 | # Unless required by applicable law or agreed to in writing, software # 14 | # distributed under the License is distributed on an "AS IS" BASIS, # 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 16 | # See the License for the specific language governing permissions and # 17 | # limitations under the License. # 18 | ############################################################################ 19 | import pytest 20 | 21 | from pygls import uris 22 | from . import unix_only, windows_only 23 | 24 | 25 | @unix_only 26 | @pytest.mark.parametrize( 27 | "path,uri", 28 | [ 29 | ("/foo/bar", "file:///foo/bar"), 30 | ("/foo/space ?bar", "file:///foo/space%20%3Fbar"), 31 | ], 32 | ) 33 | def test_from_fs_path(path, uri): 34 | assert uris.from_fs_path(path) == uri 35 | 36 | 37 | @unix_only 38 | @pytest.mark.parametrize( 39 | "uri,path", 40 | [ 41 | ("file:///foo/bar#frag", "/foo/bar"), 42 | ("file:/foo/bar#frag", "/foo/bar"), 43 | ("file:/foo/space%20%3Fbar#frag", "/foo/space ?bar"), 44 | ], 45 | ) 46 | def test_to_fs_path(uri, path): 47 | assert uris.to_fs_path(uri) == path 48 | 49 | 50 | @pytest.mark.parametrize( 51 | "uri,kwargs,new_uri", 52 | [ 53 | ("file:///foo/bar", {"path": "/baz/boo"}, "file:///baz/boo"), 54 | ( 55 | "file:///D:/hello%20world.py", 56 | {"path": "D:/hello universe.py"}, 57 | "file:///d:/hello%20universe.py", 58 | ), 59 | ], 60 | ) 61 | def test_uri_with(uri, kwargs, new_uri): 62 | assert uris.uri_with(uri, **kwargs) == new_uri 63 | 64 | 65 | @windows_only 66 | @pytest.mark.parametrize( 67 | "path,uri", 68 | [ 69 | ("c:\\far\\boo", "file:///c:/far/boo"), 70 | ("C:\\far\\space ?boo", "file:///c:/far/space%20%3Fboo"), 71 | ], 72 | ) 73 | def test_win_from_fs_path(path, uri): 74 | assert uris.from_fs_path(path) == uri 75 | 76 | 77 | @windows_only 78 | @pytest.mark.parametrize( 79 | "uri,path", 80 | [ 81 | ("file:///c:/far/boo", "c:\\far\\boo"), 82 | ("file:///C:/far/boo", "c:\\far\\boo"), 83 | ("file:///C:/far/space%20%3Fboo", "c:\\far\\space ?boo"), 84 | ], 85 | ) 86 | def test_win_to_fs_path(uri, path): 87 | assert uris.to_fs_path(uri) == path 88 | --------------------------------------------------------------------------------