├── .github
├── dependabot.yml
└── workflows
│ ├── capabilities-nvim.yml
│ ├── lsp-devtools-pr.yml
│ ├── lsp-devtools-release.yml
│ ├── pytest-lsp-pr.yml
│ └── pytest-lsp-release.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── .vscode
├── launch.json
└── settings.json
├── README.md
├── docs
├── Makefile
├── _static
│ └── custom.css
├── capabilities
│ ├── general.rst
│ ├── notebook-document.rst
│ ├── text-document.rst
│ ├── text-document
│ │ ├── call-hierachy.rst
│ │ ├── code-action.rst
│ │ ├── code-lens.rst
│ │ ├── completion.rst
│ │ ├── declaration.rst
│ │ ├── definition.rst
│ │ ├── diagnostic.rst
│ │ ├── document-color.rst
│ │ ├── document-highlight.rst
│ │ ├── document-link.rst
│ │ ├── document-symbols.rst
│ │ ├── folding-range.rst
│ │ ├── formatting.rst
│ │ ├── hover.rst
│ │ ├── implementation.rst
│ │ ├── inlay-hint.rst
│ │ ├── inline-value.rst
│ │ ├── linked-editing-range.rst
│ │ ├── moniker.rst
│ │ ├── on-type-formatting.rst
│ │ ├── publish-diagnostics.rst
│ │ ├── range-formatting.rst
│ │ ├── references.rst
│ │ ├── rename.rst
│ │ ├── selection-range.rst
│ │ ├── semantic-tokens.rst
│ │ ├── signature-help.rst
│ │ ├── synchronization.rst
│ │ ├── type-definition.rst
│ │ └── type-hierachy.rst
│ ├── window.rst
│ ├── window
│ │ ├── progress.rst
│ │ ├── show-document.rst
│ │ └── show-message.rst
│ ├── workspace.rst
│ └── workspace
│ │ ├── apply-edit.rst
│ │ ├── code-lens.rst
│ │ ├── configuration.rst
│ │ ├── diagnostics.rst
│ │ ├── did-change-configuration.rst
│ │ ├── did-change-watched-files.rst
│ │ ├── execute-command.rst
│ │ ├── file-operations.rst
│ │ ├── inlay-hint.rst
│ │ ├── inline-value.rst
│ │ ├── semantic-tokens.rst
│ │ ├── symbol.rst
│ │ ├── workspace-edit.rst
│ │ └── workspace-folders.rst
├── conf.py
├── ext
│ ├── capabilities.py
│ └── supported_clients.py
├── images
│ ├── lsp-devtools-architecture.svg
│ ├── record-client-capabilities.svg
│ ├── record-example.svg
│ ├── record-log-messages.svg
│ └── tui-screenshot.svg
├── index.rst
├── lsp-devtools
│ ├── changelog.md
│ ├── guide.rst
│ └── guide
│ │ ├── example-to-file-output.json
│ │ ├── getting-started.rst
│ │ ├── inspect-command.rst
│ │ └── record-command.rst
├── make.bat
├── pytest-lsp
│ ├── changelog.md
│ ├── guide.rst
│ ├── guide
│ │ ├── client-capabilities-error.txt
│ │ ├── client-capabilities-ignore.txt
│ │ ├── client-capabilities-output.txt
│ │ ├── client-capabilities.rst
│ │ ├── fixtures.rst
│ │ ├── getting-started-fail-output.txt
│ │ ├── getting-started.rst
│ │ ├── language-client.rst
│ │ ├── troubleshooting.rst
│ │ └── window-log-message-output.txt
│ ├── howto.rst
│ ├── howto
│ │ ├── integrate-with-lsp-devtools.rst
│ │ ├── migrate-to-v1.rst
│ │ └── testing-json-rpc-servers.rst
│ └── reference.rst
└── requirements.txt
├── flake.lock
├── flake.nix
├── lib
├── lsp-devtools
│ ├── .bumpversion.cfg
│ ├── .gitignore
│ ├── CHANGES.md
│ ├── LICENSE
│ ├── MANIFEST.in
│ ├── README.md
│ ├── changes
│ │ └── github-template.html
│ ├── flake.lock
│ ├── flake.nix
│ ├── hatch.toml
│ ├── lsp_devtools
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ ├── agent
│ │ │ ├── __init__.py
│ │ │ ├── agent.py
│ │ │ ├── client.py
│ │ │ ├── protocol.py
│ │ │ └── server.py
│ │ ├── cli.py
│ │ ├── client
│ │ │ ├── __init__.py
│ │ │ ├── app.css
│ │ │ ├── editor
│ │ │ │ ├── __init__.py
│ │ │ │ ├── completion.py
│ │ │ │ └── text_editor.py
│ │ │ └── lsp.py
│ │ ├── database.py
│ │ ├── handlers
│ │ │ ├── __init__.py
│ │ │ ├── dbinit.sql
│ │ │ └── sql.py
│ │ ├── inspector
│ │ │ ├── __init__.py
│ │ │ └── app.css
│ │ ├── py.typed
│ │ └── record
│ │ │ ├── __init__.py
│ │ │ ├── filters.py
│ │ │ ├── formatters.py
│ │ │ └── visualize.py
│ ├── nix
│ │ └── lsp-devtools-overlay.nix
│ ├── pyproject.toml
│ ├── ruff.toml
│ ├── ruff_defaults.toml
│ └── tests
│ │ ├── record
│ │ ├── test_filters.py
│ │ ├── test_formatters.py
│ │ └── test_record.py
│ │ ├── servers
│ │ └── simple.py
│ │ └── test_agent.py
└── pytest-lsp
│ ├── .bumpversion.cfg
│ ├── .gitignore
│ ├── CHANGES.md
│ ├── LICENSE
│ ├── MANIFEST.in
│ ├── Makefile
│ ├── README.md
│ ├── changes
│ └── github-template.html
│ ├── flake.lock
│ ├── flake.nix
│ ├── hatch.toml
│ ├── nix
│ └── pytest-lsp-overlay.nix
│ ├── pyproject.toml
│ ├── pytest_lsp
│ ├── __init__.py
│ ├── checks.py
│ ├── client.py
│ ├── clients
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── emacs_v29.1.json
│ │ ├── neovim_v0.10.0.json
│ │ ├── neovim_v0.6.1.json
│ │ ├── neovim_v0.7.0.json
│ │ ├── neovim_v0.8.0.json
│ │ ├── neovim_v0.9.1.json
│ │ └── visual_studio_code_v1.65.2.json
│ ├── plugin.py
│ ├── protocol.py
│ └── py.typed
│ ├── ruff.toml
│ ├── ruff_defaults.toml
│ └── tests
│ ├── conftest.py
│ ├── examples
│ ├── client-capabilities
│ │ ├── server.py
│ │ └── t_server.py
│ ├── diagnostics
│ │ ├── server.py
│ │ └── t_server.py
│ ├── fixture-passthrough
│ │ ├── server.py
│ │ └── t_server.py
│ ├── fixture-scope
│ │ ├── server.py
│ │ └── t_server.py
│ ├── generic-rpc
│ │ ├── server.py
│ │ └── t_server.py
│ ├── getting-started-fail
│ │ ├── server.py
│ │ └── t_server.py
│ ├── getting-started
│ │ ├── server.py
│ │ └── t_server.py
│ ├── parameterised-clients
│ │ ├── server.py
│ │ └── t_server.py
│ ├── ruff.toml
│ ├── server-stderr
│ │ ├── server.py
│ │ └── t_server.py
│ ├── window-create-progress
│ │ ├── server.py
│ │ └── t_server.py
│ ├── window-log-message-fail
│ │ ├── server.py
│ │ └── t_server.py
│ ├── window-log-message
│ │ ├── server.py
│ │ └── t_server.py
│ ├── window-show-document
│ │ ├── server.py
│ │ └── t_server.py
│ ├── window-show-message
│ │ ├── server.py
│ │ └── t_server.py
│ └── workspace-configuration
│ │ ├── server.py
│ │ └── t_server.py
│ ├── servers
│ ├── capabilities.py
│ ├── completion_exit.py
│ ├── crash.py
│ ├── hello.py
│ ├── invalid_json.py
│ └── notify_exit.py
│ ├── test_checks.py
│ ├── test_client.py
│ ├── test_examples.py
│ └── test_plugin.py
└── scripts
├── check_capabilities.py
├── make_release.py
├── nvim-capabilities.lua
└── should-build.sh
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | target-branch: "develop"
11 | schedule:
12 | interval: "weekly"
13 |
--------------------------------------------------------------------------------
/.github/workflows/capabilities-nvim.yml:
--------------------------------------------------------------------------------
1 | name: Neovim Capabilities
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version:
6 | description: 'Version to capture capabilities for'
7 | required: true
8 | type: string
9 |
10 | jobs:
11 | capture:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Setup Python
17 | uses: actions/setup-python@v5
18 | with:
19 | python-version: "3.11"
20 |
21 | - run: |
22 | gh release download ${{ inputs.version }} -p '*.appimage' -R neovim/neovim
23 | ls -l
24 |
25 | chmod +x nvim-linux-x86_64.appimage
26 | ./nvim-linux-x86_64.appimage --appimage-extract
27 | ./squashfs-root/usr/bin/nvim -l scripts/nvim-capabilities.lua
28 |
29 | mv neovim_v*.json lib/pytest-lsp/pytest_lsp/clients/
30 | ls -l
31 |
32 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
33 | git config user.name "github-actions[bot]"
34 |
35 | git checkout -b neovim-${{ inputs.version }}-capabilities
36 | git add lib/pytest-lsp/pytest_lsp/clients/
37 | git commit -m "Add client capabilities for Neovim ${{ inputs.version }}"
38 | git push -u origin neovim-${{ inputs.version }}-capabilities
39 |
40 | gh pr create --base develop --fill
41 | name: Capture Neovim Capailities
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 |
--------------------------------------------------------------------------------
/.github/workflows/lsp-devtools-pr.yml:
--------------------------------------------------------------------------------
1 | name: 'PR: lsp-devtools'
2 | on:
3 | pull_request:
4 | branches:
5 | - develop
6 | - release
7 | paths:
8 | - 'lib/lsp-devtools/**'
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Setup Python
17 | uses: actions/setup-python@v5
18 | with:
19 | python-version: "3.11"
20 | cache: pip
21 | cache-dependency-path: lib/lsp-devtools/pyproject.toml
22 |
23 | - run: |
24 | python --version
25 | python -m pip install --upgrade pip
26 | python -m pip install --upgrade hatch towncrier
27 | name: Setup Environment
28 |
29 | - run: |
30 | set -e
31 | ./scripts/make_release.py lsp-devtools
32 | name: Set Version
33 |
34 | - uses: hynek/build-and-inspect-python-package@v2
35 | with:
36 | path: lib/lsp-devtools
37 |
38 | test:
39 | name: "Python v${{ matrix.python-version }} -- ${{ matrix.os }}"
40 | runs-on: ${{ matrix.os }}
41 | strategy:
42 | fail-fast: false
43 | matrix:
44 | python-version: ["3.9", "3.10", "3.11", "3.12"]
45 | os: [ubuntu-latest]
46 |
47 | steps:
48 | - uses: actions/checkout@v4
49 |
50 | - name: Setup Python ${{ matrix.python-version }}
51 | uses: actions/setup-python@v5
52 | with:
53 | allow-prereleases: true
54 | python-version: ${{ matrix.python-version }}
55 | cache: pip
56 | cache-dependency-path: lib/lsp-devtools/pyproject.toml
57 |
58 | - run: |
59 | python --version
60 | python -m pip install --upgrade pip
61 | python -m pip install --upgrade hatch
62 | name: Setup Environment
63 |
64 | - run: |
65 | cd lib/lsp-devtools
66 | hatch test -i py=${{ matrix.python-version }}
67 | shell: bash
68 | name: Run Tests
69 |
--------------------------------------------------------------------------------
/.github/workflows/lsp-devtools-release.yml:
--------------------------------------------------------------------------------
1 | name: 'Release: lsp-devtools'
2 |
3 | on:
4 | push:
5 | branches:
6 | - release
7 | paths:
8 | - 'lib/lsp-devtools/**'
9 |
10 | jobs:
11 | release:
12 | name: lsp-devtools release
13 | runs-on: ubuntu-latest
14 | environment:
15 | name: pypi
16 | url: https://pypi.org/p/lsp-devtools
17 | permissions:
18 | contents: write
19 | id-token: write
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 |
24 | - uses: actions/setup-python@v5
25 | with:
26 | python-version: "3.10"
27 |
28 | - run: |
29 | python --version
30 | python -m pip install --upgrade pip
31 | python -m pip install hatch towncrier docutils
32 | name: Install Build Tools
33 |
34 | - run: |
35 | set -e
36 |
37 | ./scripts/make_release.py lsp-devtools
38 | name: Set Version
39 | id: info
40 |
41 | - name: Package
42 | run: |
43 | cd lib/lsp-devtools
44 | hatch build
45 |
46 | - name: 'Upload Artifact'
47 | uses: actions/upload-artifact@v4
48 | with:
49 | name: 'dist'
50 | path: lib/lsp-devtools/dist
51 |
52 | - name: Publish
53 | uses: pypa/gh-action-pypi-publish@release/v1
54 | with:
55 | packages-dir: lib/lsp-devtools/dist/
56 |
57 | - name: Create Release
58 | run: |
59 | gh release create "${RELEASE_TAG}" \
60 | --title "lsp-devtools v${VERSION} - ${RELEASE_DATE}" \
61 | -F lib/lsp-devtools/.changes.html \
62 | ./lib/lsp-devtools/dist/*
63 | env:
64 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65 |
--------------------------------------------------------------------------------
/.github/workflows/pytest-lsp-pr.yml:
--------------------------------------------------------------------------------
1 | name: 'PR: pytest-lsp'
2 | on:
3 | pull_request:
4 | branches:
5 | - develop
6 | - release
7 | paths:
8 | - 'lib/pytest-lsp/**'
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Setup Python
17 | uses: actions/setup-python@v5
18 | with:
19 | python-version: "3.11"
20 | cache: pip
21 | cache-dependency-path: lib/pytest-lsp/pyproject.toml
22 |
23 | - run: |
24 | python --version
25 | python -m pip install --upgrade pip
26 | python -m pip install --upgrade hatch towncrier
27 | name: Setup Environment
28 |
29 | - run: |
30 | set -e
31 | ./scripts/make_release.py pytest-lsp
32 | name: Set Version
33 |
34 | - uses: hynek/build-and-inspect-python-package@v2
35 | with:
36 | path: lib/pytest-lsp
37 |
38 | test:
39 | name: "Python v${{ matrix.python-version }} -- ${{ matrix.os }}"
40 | runs-on: ${{ matrix.os }}
41 | strategy:
42 | fail-fast: false
43 | matrix:
44 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
45 | os: [ubuntu-latest, windows-latest]
46 |
47 | steps:
48 | - uses: actions/checkout@v4
49 |
50 | - name: Setup Python ${{ matrix.python-version }}
51 | uses: actions/setup-python@v5
52 | with:
53 | allow-prereleases: true
54 | python-version: ${{ matrix.python-version }}
55 | cache: pip
56 | cache-dependency-path: lib/pytest-lsp/pyproject.toml
57 |
58 | - run: |
59 | python --version
60 | python -m pip install --upgrade pip
61 | python -m pip install --upgrade hatch
62 | name: Setup Environment
63 |
64 | - run: |
65 | cd lib/pytest-lsp
66 | hatch test -i py=${{ matrix.python-version }}
67 | shell: bash
68 | name: Run Tests
69 |
--------------------------------------------------------------------------------
/.github/workflows/pytest-lsp-release.yml:
--------------------------------------------------------------------------------
1 | name: 'Release: pytest-lsp'
2 |
3 | on:
4 | push:
5 | branches:
6 | - release
7 | paths:
8 | - 'lib/pytest-lsp/**'
9 |
10 | jobs:
11 | release:
12 | name: pytest-lsp release
13 | runs-on: ubuntu-latest
14 | environment:
15 | name: pypi
16 | url: https://pypi.org/p/pytest-lsp
17 | permissions:
18 | contents: write
19 | id-token: write
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 |
24 | - uses: actions/setup-python@v5
25 | with:
26 | python-version: "3.10"
27 |
28 | - run: |
29 | python --version
30 | python -m pip install --upgrade pip
31 | python -m pip install tox hatch towncrier docutils
32 | name: Setup Environment
33 |
34 | - run: |
35 | set -e
36 |
37 | ./scripts/make_release.py pytest-lsp
38 | name: Set Version
39 | id: info
40 |
41 | - name: Package
42 | run: |
43 | cd lib/pytest-lsp
44 | hatch build
45 |
46 | - name: 'Upload Artifact'
47 | uses: actions/upload-artifact@v4
48 | with:
49 | name: 'dist'
50 | path: lib/pytest-lsp/dist
51 |
52 | - name: Publish
53 | uses: pypa/gh-action-pypi-publish@release/v1
54 | with:
55 | packages-dir: lib/pytest-lsp/dist/
56 |
57 | - name: Create Release
58 | run: |
59 | gh release create "${RELEASE_TAG}" \
60 | --title "pytest-lsp v${VERSION} - ${RELEASE_DATE}" \
61 | -F lib/pytest-lsp/.changes.html \
62 | ./lib/pytest-lsp/dist/*
63 | env:
64 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .coverage
2 | .env
3 | .tox
4 | *.pyc
5 |
6 | dist
7 | build
8 | node_modules
9 | .mypy_cache
10 | __pycache__
11 | *.egg-info
12 | _build
13 | *.*~
14 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | exclude: '.bumpversion.cfg$'
2 | repos:
3 |
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v5.0.0
6 | hooks:
7 | - id: check-yaml
8 | - id: end-of-file-fixer
9 | exclude: 'lib/pytest-lsp/pytest_lsp/clients/.*\.json'
10 | - id: trailing-whitespace
11 |
12 | - repo: https://github.com/astral-sh/ruff-pre-commit
13 | rev: v0.11.9
14 | hooks:
15 | - id: ruff
16 | args: [--fix]
17 | files: 'lib/.*\.py'
18 |
19 | - id: ruff-format
20 | files: 'lib/.*\.py'
21 |
22 | - repo: https://github.com/pre-commit/mirrors-mypy
23 | rev: 'v1.15.0'
24 | hooks:
25 | - id: mypy
26 | name: mypy (pytest-lsp)
27 | args: [--explicit-package-bases,--check-untyped-defs]
28 | additional_dependencies:
29 | - importlib-resources
30 | - platformdirs
31 | - 'pygls>=2.0a2'
32 | - pytest
33 | - pytest-asyncio
34 | - websockets
35 | files: 'lib/pytest-lsp/pytest_lsp/.*\.py'
36 |
37 | - id: mypy
38 | name: mypy (lsp-devtools)
39 | args: [--explicit-package-bases,--check-untyped-defs]
40 | additional_dependencies:
41 | - aiosqlite
42 | - attrs
43 | - importlib-resources
44 | - platformdirs
45 | - 'pygls>=2.0a2'
46 | - stamina
47 | - textual
48 | - websockets
49 | files: 'lib/lsp-devtools/lsp_devtools/.*\.py'
50 |
51 | - repo: local
52 | hooks:
53 | - id: check-capabilities
54 | name: check-capabilities
55 | language: python
56 | additional_dependencies:
57 | - lsprotocol
58 | files: 'lib/pytest-lsp/pytest_lsp/clients/.*\.json'
59 | entry: python scripts/check_capabilities.py
60 |
--------------------------------------------------------------------------------
/.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/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 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "LSP Inspector",
9 | "type": "python",
10 | "request": "launch",
11 | "module": "lsp_devtools",
12 | "args": [
13 | "inspector"
14 | ]
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[python]": {
3 | "editor.defaultFormatter": "ms-python.black-formatter",
4 | "editor.formatOnSave": true,
5 | "editor.codeActionsOnSave": {
6 | "source.organizeImports": true
7 | }
8 | },
9 | "isort.args": [
10 | "--settings-file",
11 | "./lib/lsp-devtools/pyproject.toml"
12 | ],
13 | "files.exclude": {
14 | "**/.git": true,
15 | "**/.svn": true,
16 | "**/.hg": true,
17 | "**/CVS": true,
18 | "**/.DS_Store": true,
19 | "**/Thumbs.db": true,
20 | "**/__pycache__": true,
21 | "**/*.egg-info": true,
22 | "**/.pytest_cache": true,
23 | "**/.mypy_cache": true,
24 | },
25 | "esbonio.sphinx.buildCommand": [
26 | "sphinx-build",
27 | "-M",
28 | "html",
29 | "docs",
30 | "docs/_build"
31 | ],
32 | "python.testing.pytestArgs": [
33 | "lib/pytest-lsp/tests"
34 | ],
35 | "python.testing.unittestEnabled": false,
36 | "python.testing.pytestEnabled": true,
37 | }
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
LSP Devtools
2 |
3 | [](https://results.pre-commit.ci/latest/github/swyddfa/lsp-devtools/develop)
4 |
5 | This repo is an attempt at building the developer tooling I wished existed when I first started working on [Esbonio](https://github.com/swyddfa/esbonio/).
6 |
7 | This is a monorepo containing a number of sub-projects.
8 |
9 | ## `lib/lsp-devtools` - A grab bag of development utilities
10 |
11 | [](https://pypi.org/project/lsp-devtools)[](https://pypistats.org/packages/lsp-devtools)[](https://github.com/swyddfa/lsp-devtools/blob/develop/lib/lsp-devtools/LICENSE)
12 |
13 | 
14 |
15 | A collection of cli utilities aimed at aiding the development of language servers and/or clients.
16 |
17 | - `agent`: Used to wrap an lsp server allowing messages sent between it and the client to be intercepted and inspected by other tools.
18 | - `record`: Connects to an agent and record traffic to file, sqlite db or console. Supports filtering and formatting the output
19 | - `inspect`: A browser devtools inspired TUI to visualise and inspecting LSP traffic. Powered by [textual](https://textual.textualize.io/)
20 | - `client`: **Experimental** A TUI language client with built in `inspect` panel. Powered by [textual](https://textual.textualize.io/)
21 |
22 | ## `lib/pytest-lsp` - End-to-end testing of language servers with pytest
23 |
24 | [](https://pypi.org/project/pytest-lsp)[](https://pypistats.org/packages/pytest-lsp)[](https://github.com/swyddfa/lsp-devtools/blob/develop/lib/pytest-lsp/LICENSE)
25 |
26 | `pytest-lsp` is a pytest plugin for writing end-to-end tests for language servers.
27 |
28 | It works by running the language server in a subprocess and communicating with it over stdio, just like a real language client.
29 | This also means `pytest-lsp` can be used to test language servers written in any language - not just Python.
30 |
31 | `pytest-lsp` relies on the [`pygls`](https://github.com/openlawlibrary/pygls) library for its language server protocol implementation.
32 |
33 | ```python
34 | import sys
35 |
36 | import pytest
37 | import pytest_lsp
38 | from lsprotocol import types
39 | from pytest_lsp import (
40 | ClientServerConfig,
41 | LanguageClient,
42 | client_capabilities,
43 | )
44 |
45 |
46 | @pytest_lsp.fixture(
47 | scope="module",
48 | config=ClientServerConfig(
49 | server_command=[sys.executable, "-m", "esbonio"],
50 | ),
51 | )
52 | async def client(lsp_client: LanguageClient):
53 | # Setup
54 | response = await lsp_client.initialize_session(
55 | types.InitializeParams(
56 | capabilities=client_capabilities("visual-studio-code"),
57 | workspace_folders=[
58 | types.WorkspaceFolder(
59 | uri="file:///path/to/test/project/root/", name="project"
60 | ),
61 | ],
62 | )
63 | )
64 |
65 | yield
66 |
67 | # Teardown
68 | await lsp_client.shutdown_session()
69 |
70 |
71 | @pytest.mark.asyncio(loop_scope="module")
72 | async def test_completion(client: LanguageClient):
73 | result = await client.text_document_completion_async(
74 | params=types.CompletionParams(
75 | position=types.Position(line=5, character=23),
76 | text_document=types.TextDocumentIdentifier(
77 | uri="file:///path/to/test/project/root/test_file.rst"
78 | ),
79 | )
80 | )
81 |
82 | assert len(result.items) > 0
83 | ```
84 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | ifeq ($(CI),true)
2 | BUILD=html-build
3 | else
4 | BUILD=html-local
5 | endif
6 |
7 |
8 | ifeq ($(GITHUB_REF),refs/heads/release)
9 | BUILDDIR=stable
10 | else
11 | BUILDDIR=latest
12 | endif
13 |
14 |
15 | html-build:
16 | BUILDDIR=$(BUILDDIR) sphinx-build -b html . _build/$(BUILDDIR)/en/
17 | echo "version=$(BUILDDIR)" >> $(GITHUB_OUTPUT)
18 |
19 |
20 | html-local:
21 | sphinx-build -M html . _build $(SPHINXOPTS)
22 |
23 | html: $(BUILD)
24 |
--------------------------------------------------------------------------------
/docs/_static/custom.css:
--------------------------------------------------------------------------------
1 | .scrollable-svg {
2 | max-height: 450px;
3 | overflow: auto;
4 | }
5 |
--------------------------------------------------------------------------------
/docs/capabilities/general.rst:
--------------------------------------------------------------------------------
1 | ``general``
2 | ===========
3 |
4 | .. default-domain:: capabilities
5 |
6 | .. values-table:: general.position_encodings
7 | :value-set: PositionEncodingKind
8 |
9 | ``staleRequestSupport``
10 | -----------------------
11 |
12 | .. bool-table:: general.stale_request_support.cancel
13 |
14 | .. values-table:: general.stale_request_support.retry_on_content_modified
15 |
16 | ``regularExpressions``
17 | ----------------------
18 |
19 | .. values-table:: general.regular_expressions.engine
20 |
21 | .. values-table:: general.regular_expressions.version
22 |
23 | ``markdown``
24 | ------------
25 |
26 | .. values-table:: general.markdown.parser
27 |
28 | .. values-table:: general.markdown.version
29 |
30 | .. values-table:: general.markdown.allowed_tags
31 |
--------------------------------------------------------------------------------
/docs/capabilities/notebook-document.rst:
--------------------------------------------------------------------------------
1 | ``notebook``
2 | ============
3 |
4 | .. default-domain:: capabilities
5 |
6 | ``synchronization``
7 | -------------------
8 |
9 | .. bool-table:: notebook_document.synchronization.dynamic_registration
10 |
11 | .. bool-table:: notebook_document.synchronization.execution_summary_support
12 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document.rst:
--------------------------------------------------------------------------------
1 | ``textDocument``
2 | ================
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 | :glob:
7 |
8 | text-document/*
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/call-hierachy.rst:
--------------------------------------------------------------------------------
1 | ``callHierarchy``
2 | =================
3 |
4 | Capabilities relating to the :lsp:`textDocument/prepareCallHierarchy` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.call_hierarchy.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/code-action.rst:
--------------------------------------------------------------------------------
1 | ``codeAction``
2 | ==============
3 |
4 | Capabilities relating to the :lsp:`textDocument/codeAction` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.code_action.dynamic_registration
9 |
10 | .. values-table:: text_document.code_action.code_action_literal_support.code_action_kind.value_set
11 | :value-set: CodeActionKind
12 |
13 | .. bool-table:: text_document.code_action.is_preferred_support
14 |
15 | .. bool-table:: text_document.code_action.disabled_support
16 |
17 | .. bool-table:: text_document.code_action.data_support
18 |
19 | .. values-table:: text_document.code_action.resolve_support.properties
20 |
21 | .. bool-table:: text_document.code_action.honors_change_annotations
22 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/code-lens.rst:
--------------------------------------------------------------------------------
1 | ``codeLens``
2 | ============
3 |
4 | Capabilities relating to the :lsp:`textDocument/codeLens` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.code_lens.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/completion.rst:
--------------------------------------------------------------------------------
1 | ``completion``
2 | ==============
3 |
4 | Capabilities relating to the :lsp:`textDocument/completion` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.completion.dynamic_registration
9 |
10 | .. bool-table:: text_document.completion.context_support
11 |
12 | .. values-table:: text_document.completion.insert_text_mode
13 |
14 | ``completionItem``
15 | ------------------
16 |
17 | .. bool-table:: text_document.completion.completion_item.snippet_support
18 |
19 | .. bool-table:: text_document.completion.completion_item.commit_characters_support
20 |
21 | .. values-table:: text_document.completion.completion_item.documentation_format
22 | :value-set: MarkupKind
23 |
24 | .. bool-table:: text_document.completion.completion_item.deprecated_support
25 |
26 | .. bool-table:: text_document.completion.completion_item.preselect_support
27 |
28 | .. bool-table:: text_document.completion.completion_item.insert_replace_support
29 |
30 | .. bool-table:: text_document.completion.completion_item.label_details_support
31 |
32 | ``insertTextModeSupport``
33 | ^^^^^^^^^^^^^^^^^^^^^^^^^
34 |
35 | .. values-table:: text_document.completion.completion_item.insert_text_mode_support.value_set
36 | :value-set: InsertTextMode
37 |
38 | ``resolveSupport``
39 | ^^^^^^^^^^^^^^^^^^
40 |
41 | .. values-table:: text_document.completion.completion_item.resolve_support.properties
42 |
43 |
44 | ``tagSupport``
45 | ^^^^^^^^^^^^^^
46 |
47 | .. values-table:: text_document.completion.completion_item.tag_support.value_set
48 | :value-set: CompletionItemTag
49 |
50 |
51 | ``completionItemKind``
52 | ----------------------
53 |
54 | .. values-table:: text_document.completion.completion_item_kind.value_set
55 | :value-set: CompletionItemKind
56 |
57 | ``completionList``
58 | ------------------
59 |
60 | .. values-table:: text_document.completion.completion_list.item_defaults
61 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/declaration.rst:
--------------------------------------------------------------------------------
1 | ``declaration``
2 | ===============
3 |
4 | Capabilities relating to the :lsp:`textDocument/declaration` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.declaration.dynamic_registration
9 |
10 | .. bool-table:: text_document.declaration.link_support
11 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/definition.rst:
--------------------------------------------------------------------------------
1 | ``definition``
2 | ==============
3 |
4 | Capabilities relating to the :lsp:`textDocument/definition` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.definition.dynamic_registration
9 |
10 | .. bool-table:: text_document.definition.link_support
11 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/diagnostic.rst:
--------------------------------------------------------------------------------
1 | ``diagnostic``
2 | ==============
3 |
4 | Capabilities relating to the :lsp:`textDocument/diagnostic` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.diagnostic.dynamic_registration
9 |
10 | .. bool-table:: text_document.diagnostic.related_document_support
11 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/document-color.rst:
--------------------------------------------------------------------------------
1 | ``documentColor``
2 | =================
3 |
4 | Capabilities relating to the :lsp:`textDocument/documentColor` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.color_provider.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/document-highlight.rst:
--------------------------------------------------------------------------------
1 | ``documentHighlight``
2 | =====================
3 |
4 | Capabilities relating to the :lsp:`textDocument/documentHighlight` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.document_highlight.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/document-link.rst:
--------------------------------------------------------------------------------
1 | ``documentLink``
2 | ================
3 |
4 | Capabilities relating to the :lsp:`textDocument/documentLink` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.document_link.dynamic_registration
9 |
10 | .. bool-table:: text_document.document_link.tooltip_support
11 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/document-symbols.rst:
--------------------------------------------------------------------------------
1 | ``documentSymbols``
2 | ===================
3 |
4 | Capabilities relating to the :lsp:`textDocument/documentSymbols` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.document_symbol.dynamic_registration
9 |
10 | .. bool-table:: text_document.document_symbol.hierarchical_document_symbol_support
11 |
12 | .. bool-table:: text_document.document_symbol.label_support
13 |
14 |
15 | ``symbolKind``
16 | --------------
17 |
18 | .. values-table:: text_document.document_symbol.symbol_kind.value_set
19 | :value-set: SymbolKind
20 |
21 | ``tagSupport``
22 | --------------
23 |
24 | .. values-table:: text_document.document_symbol.tag_support.value_set
25 | :value-set: SymbolTag
26 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/folding-range.rst:
--------------------------------------------------------------------------------
1 | ``foldingRange``
2 | ================
3 |
4 | Capabilities relating to the :lsp:`textDocument/foldingRange` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.folding_range.dynamic_registration
9 |
10 | .. values-table:: text_document.folding_range.range_limit
11 |
12 | .. bool-table:: text_document.folding_range.line_folding_only
13 |
14 | ``foldingRange``
15 | ----------------
16 |
17 | .. bool-table:: text_document.folding_range.folding_range.collapsed_text
18 |
19 |
20 | ``foldingRangeKind``
21 | --------------------
22 |
23 | .. values-table:: text_document.folding_range.folding_range_kind.value_set
24 | :value-set: FoldingRangeKind
25 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/formatting.rst:
--------------------------------------------------------------------------------
1 | ``formatting``
2 | ==============
3 |
4 | Capabilities relating to the :lsp:`textDocument/formatting` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.formatting.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/hover.rst:
--------------------------------------------------------------------------------
1 | ``hover``
2 | =========
3 |
4 | Capabilities relating to the :lsp:`textDocument/hover` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.hover.dynamic_registration
9 |
10 | .. values-table:: text_document.hover.content_format
11 | :value-set: MarkupKind
12 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/implementation.rst:
--------------------------------------------------------------------------------
1 | ``implementation``
2 | ==================
3 |
4 | Capabilities relating to the :lsp:`textDocument/implementation` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.implementation.dynamic_registration
9 |
10 | .. bool-table:: text_document.implementation.link_support
11 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/inlay-hint.rst:
--------------------------------------------------------------------------------
1 | ``inlayHint``
2 | =============
3 |
4 | Capabilities relating to the :lsp:`textDocument/inlayHint` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.inlay_hint.dynamic_registration
9 |
10 | ``resolveSupport``
11 | ------------------
12 |
13 | .. values-table:: text_document.inlay_hint.resolve_support.properties
14 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/inline-value.rst:
--------------------------------------------------------------------------------
1 | ``inlineValue``
2 | ===============
3 |
4 | Capabilities relating to the :lsp:`textDocument/inlineValue` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.inline_value.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/linked-editing-range.rst:
--------------------------------------------------------------------------------
1 | ``linkedEditingRange``
2 | ======================
3 |
4 | Capabilities relating to the :lsp:`textDocument/linkedEditingRange` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.linked_editing_range.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/moniker.rst:
--------------------------------------------------------------------------------
1 | ``moniker``
2 | ===========
3 |
4 | Capabilities relating to the :lsp:`textDocument/moniker` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.moniker.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/on-type-formatting.rst:
--------------------------------------------------------------------------------
1 | ``onTypeFormatting``
2 | ====================
3 |
4 | Capabilities relating to the :lsp:`textDocument/onTypeFormatting` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.on_type_formatting.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/publish-diagnostics.rst:
--------------------------------------------------------------------------------
1 | ``publishDiagnostics``
2 | ======================
3 |
4 | Capabilities relating to the :lsp:`textDocument/publishDiagnostics` notification
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.publish_diagnostics.related_information
9 |
10 | .. bool-table:: text_document.publish_diagnostics.version_support
11 |
12 | .. bool-table:: text_document.publish_diagnostics.code_description_support
13 |
14 | .. bool-table:: text_document.publish_diagnostics.data_support
15 |
16 | ``tagSupport``
17 | --------------
18 |
19 | .. values-table:: text_document.publish_diagnostics.tag_support.value_set
20 | :value-set: DiagnosticTag
21 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/range-formatting.rst:
--------------------------------------------------------------------------------
1 | ``rangeFormatting``
2 | ===================
3 |
4 | Capabilities relating to the :lsp:`textDocument/rangeFormatting` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.range_formatting.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/references.rst:
--------------------------------------------------------------------------------
1 | ``references``
2 | ==============
3 |
4 | Capabilities relating to the :lsp:`textDocument/references` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.references.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/rename.rst:
--------------------------------------------------------------------------------
1 | ``rename``
2 | ==========
3 |
4 | Capabilities relating to the :lsp:`textDocument/rename` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.rename.dynamic_registration
9 |
10 | .. bool-table:: text_document.rename.prepare_support
11 |
12 | .. bool-table:: text_document.rename.honors_change_annotations
13 |
14 |
15 | ``prepareSupportDefaultBehavior``
16 | ---------------------------------
17 |
18 | .. values-table:: text_document.rename.prepare_support_default_behavior
19 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/selection-range.rst:
--------------------------------------------------------------------------------
1 | ``selectionRange``
2 | ==================
3 |
4 | Capabilities relating to the :lsp:`textDocument/selectionRange` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.selection_range.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/semantic-tokens.rst:
--------------------------------------------------------------------------------
1 | ``semanticTokens``
2 | ==================
3 |
4 | Capabilities relating to :lsp:`textDocument/semanticTokens`.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.semantic_tokens.dynamic_registration
9 |
10 | .. bool-table:: text_document.semantic_tokens.requests.range
11 |
12 | .. bool-table:: text_document.semantic_tokens.requests.full
13 |
14 | .. bool-table:: text_document.semantic_tokens.requests.full.delta
15 |
16 | .. values-table:: text_document.semantic_tokens.token_types
17 | :value-set: SemanticTokenTypes
18 |
19 | .. values-table:: text_document.semantic_tokens.token_modifiers
20 | :value-set: SemanticTokenModifiers
21 |
22 | .. bool-table:: text_document.semantic_tokens.overlapping_token_support
23 |
24 | .. bool-table:: text_document.semantic_tokens.multiline_token_support
25 |
26 | .. bool-table:: text_document.semantic_tokens.server_cancel_support
27 |
28 | .. bool-table:: text_document.semantic_tokens.augments_syntax_tokens
29 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/signature-help.rst:
--------------------------------------------------------------------------------
1 | ``signatureHelp``
2 | =================
3 |
4 | Capabilities relating to the :lsp:`textDocument/signatureHelp` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.signature_help.dynamic_registration
9 |
10 | .. bool-table:: text_document.signature_help.context_support
11 |
12 | ``signatureInformation``
13 | ------------------------
14 |
15 | .. bool-table:: text_document.signature_help.signature_information.active_parameter_support
16 |
17 |
18 | ``parameterInformation``
19 | ^^^^^^^^^^^^^^^^^^^^^^^^
20 |
21 | .. bool-table:: text_document.signature_help.signature_information.parameter_information.label_offset_support
22 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/synchronization.rst:
--------------------------------------------------------------------------------
1 | ``synchronization``
2 | ===================
3 |
4 | .. default-domain:: capabilities
5 |
6 | .. bool-table:: text_document.synchronization.dynamic_registration
7 |
8 | .. bool-table:: text_document.synchronization.did_save
9 |
10 | .. bool-table:: text_document.synchronization.will_save
11 |
12 | .. bool-table:: text_document.synchronization.will_save_wait_until
13 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/type-definition.rst:
--------------------------------------------------------------------------------
1 | ``typeDefinition``
2 | ==================
3 |
4 | Capabilities relating to the :lsp:`textDocument/typeDefinition` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.type_definition.dynamic_registration
9 |
10 | .. bool-table:: text_document.type_definition.link_support
11 |
--------------------------------------------------------------------------------
/docs/capabilities/text-document/type-hierachy.rst:
--------------------------------------------------------------------------------
1 | ``typeHierarchy``
2 | =================
3 |
4 | Capabilities relating to the :lsp:`textDocument/prepareTypeHierarchy` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: text_document.type_hierarchy.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/window.rst:
--------------------------------------------------------------------------------
1 | ``window``
2 | ==========
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 | :glob:
7 |
8 | window/*
9 |
--------------------------------------------------------------------------------
/docs/capabilities/window/progress.rst:
--------------------------------------------------------------------------------
1 | ``workDoneProgress``
2 | ====================
3 |
4 | .. capabilities:bool-table:: window.work_done_progress
5 |
--------------------------------------------------------------------------------
/docs/capabilities/window/show-document.rst:
--------------------------------------------------------------------------------
1 | ``showDocument``
2 | ================
3 |
4 | Capabilities relating to the :lsp:`window/showDocument` request.
5 |
6 | .. capabilities:bool-table:: window.show_document.support
7 |
--------------------------------------------------------------------------------
/docs/capabilities/window/show-message.rst:
--------------------------------------------------------------------------------
1 | ``showMessage``
2 | ===============
3 |
4 | Capabilities relating to the :lsp:`window/showMessageRequest` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | ``messageActionItem``
9 | ---------------------
10 |
11 | .. bool-table:: window.show_message.message_action_item.additional_properties_support
12 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace.rst:
--------------------------------------------------------------------------------
1 | ``workspace``
2 | =============
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 | :glob:
7 |
8 | workspace/*
9 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/apply-edit.rst:
--------------------------------------------------------------------------------
1 | ``applyEdit``
2 | =============
3 |
4 | Capabilities relating to the :lsp:`workspace/applyEdit` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: workspace.apply_edit
9 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/code-lens.rst:
--------------------------------------------------------------------------------
1 | ``codeLens``
2 | ============
3 |
4 | Capabilities relating to the :lsp:`workspace/codeLens/refresh` request.
5 |
6 | .. capabilities:bool-table:: workspace.code_lens.refresh_support
7 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/configuration.rst:
--------------------------------------------------------------------------------
1 | ``configuration``
2 | =================
3 |
4 | Capabilities relating to the :lsp:`workspace/configuration` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: workspace.configuration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/diagnostics.rst:
--------------------------------------------------------------------------------
1 | ``diagnostics``
2 | ===============
3 |
4 | Capabilities relating to the :lsp:`workspace/diagnostic/refresh` request.
5 |
6 | .. capabilities:bool-table:: workspace.diagnostics.refresh_support
7 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/did-change-configuration.rst:
--------------------------------------------------------------------------------
1 | ``didChangeConfiguration``
2 | ==========================
3 |
4 | Capabilities relating to the :lsp:`workspace/didChangeConfiguration` notification.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: workspace.did_change_configuration.dynamic_registration
9 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/did-change-watched-files.rst:
--------------------------------------------------------------------------------
1 | ``didChangeWatchedFiles``
2 | =========================
3 |
4 | .. default-domain:: capabilities
5 |
6 | .. bool-table:: workspace.did_change_watched_files.dynamic_registration
7 |
8 | .. bool-table:: workspace.did_change_watched_files.relative_pattern_support
9 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/execute-command.rst:
--------------------------------------------------------------------------------
1 | ``executeCommand``
2 | ==================
3 |
4 | Capabilities relating to the :lsp:`workspace/executeCommand` request.
5 |
6 | .. capabilities:bool-table:: workspace.execute_command.dynamic_registration
7 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/file-operations.rst:
--------------------------------------------------------------------------------
1 | ``fileOperations``
2 | ==================
3 |
4 | .. default-domain:: capabilities
5 |
6 | .. bool-table:: workspace.file_operations.dynamic_registration
7 |
8 | .. bool-table:: workspace.file_operations.will_create
9 |
10 | .. bool-table:: workspace.file_operations.did_create
11 |
12 | .. bool-table:: workspace.file_operations.will_rename
13 |
14 | .. bool-table:: workspace.file_operations.did_rename
15 |
16 | .. bool-table:: workspace.file_operations.will_delete
17 |
18 | .. bool-table:: workspace.file_operations.did_delete
19 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/inlay-hint.rst:
--------------------------------------------------------------------------------
1 | ``inlayHint``
2 | =============
3 |
4 | Capabilities relating to the :lsp:`workspace/inlayHint/refresh` request.
5 |
6 | .. capabilities:bool-table:: workspace.inlay_hint.refresh_support
7 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/inline-value.rst:
--------------------------------------------------------------------------------
1 | ``inlineValue``
2 | ===============
3 |
4 | Capabilities relating to the :lsp:`workspace/inlineValue/refresh` request.
5 |
6 | .. capabilities:bool-table:: workspace.inline_value.refresh_support
7 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/semantic-tokens.rst:
--------------------------------------------------------------------------------
1 | ``semanticTokens``
2 | ==================
3 |
4 | Capabilities relating to the :lsp:`workspace/semanticTokens/refresh` request.
5 |
6 | .. capabilities:bool-table:: workspace.semantic_tokens.refresh_support
7 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/symbol.rst:
--------------------------------------------------------------------------------
1 | ``symbol``
2 | ==========
3 |
4 | Capabilities relating to the :lsp:`workspace/symbol` request.
5 |
6 | .. default-domain:: capabilities
7 |
8 | .. bool-table:: workspace.symbol.dynamic_registration
9 |
10 | ``symbolKind``
11 | --------------
12 |
13 | .. values-table:: workspace.symbol.symbol_kind.value_set
14 | :value-set: SymbolKind
15 |
16 | ``tagSupport``
17 | --------------
18 |
19 | .. values-table:: workspace.symbol.tag_support.value_set
20 | :value-set: SymbolTag
21 |
22 | ``resolveSupport``
23 | ------------------
24 |
25 | .. values-table:: workspace.symbol.resolve_support.properties
26 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/workspace-edit.rst:
--------------------------------------------------------------------------------
1 | ``workspaceEdit``
2 | =================
3 |
4 | .. default-domain:: capabilities
5 |
6 | .. bool-table:: workspace.workspace_edit.document_changes
7 |
8 | .. values-table:: workspace.workspace_edit.resource_operations
9 | :value-set: ResourceOperationKind
10 |
11 | .. values-table:: workspace.workspace_edit.failure_handling
12 |
13 | .. bool-table:: workspace.workspace_edit.normalizes_line_endings
14 |
15 | ``changeAnnotationSupport``
16 | ---------------------------
17 |
18 | .. bool-table:: workspace.workspace_edit.change_annotation_support.groups_on_label
19 |
--------------------------------------------------------------------------------
/docs/capabilities/workspace/workspace-folders.rst:
--------------------------------------------------------------------------------
1 | ``workspaceFolders``
2 | ====================
3 |
4 | Capabilities relating to the :lsp:`workspace/workspaceFolders` request.
5 |
6 | .. capabilities:bool-table:: workspace.workspace_folders
7 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 | import os
9 | import sys
10 |
11 | sys.path.insert(0, os.path.abspath("./ext"))
12 |
13 | from docutils import nodes # noqa: E402
14 | from sphinx.application import Sphinx # noqa: E402
15 |
16 | DEV_BUILD = os.getenv("BUILDDIR", None) == "latest"
17 | BRANCH = "develop" if DEV_BUILD else "release"
18 |
19 | project = "LSP Devtools"
20 | copyright = "2023, Alex Carney"
21 | author = "Alex Carney"
22 |
23 | # -- General configuration ---------------------------------------------------
24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
25 |
26 | extensions = [
27 | # built-in extensions
28 | "sphinx.ext.autodoc",
29 | "sphinx.ext.napoleon",
30 | "sphinx.ext.intersphinx",
31 | # 3rd party extensions
32 | "myst_parser",
33 | "sphinx_copybutton",
34 | "sphinx_design",
35 | # local extensions
36 | "capabilities",
37 | "supported_clients",
38 | ]
39 |
40 | autoclass_content = "both"
41 | autodoc_member_order = "groupwise"
42 | autodoc_typehints = "description"
43 |
44 | intersphinx_mapping = {
45 | "pygls": ("https://pygls.readthedocs.io/en/latest/", None),
46 | "python": ("https://docs.python.org/3/", None),
47 | "pytest": ("https://docs.pytest.org/en/stable/", None),
48 | }
49 |
50 | templates_path = ["_templates"]
51 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
52 |
53 |
54 | # -- Options for HTML output -------------------------------------------------
55 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
56 |
57 | html_theme = "furo"
58 | html_title = "LSP Devtools"
59 | html_static_path = ["_static"]
60 | html_theme_options = {
61 | "source_repository": "https://github.com/swyddfa/lsp-devtools/",
62 | "source_branch": BRANCH,
63 | "source_directory": "docs/",
64 | }
65 |
66 | if DEV_BUILD:
67 | html_theme_options["announcement"] = (
68 | "This is the unstable version of the documentation, features may change or "
69 | "be removed without warning. "
70 | 'Click here '
71 | "to view the released version"
72 | )
73 |
74 |
75 | def lsp_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
76 | """Link to sections within the lsp specification."""
77 |
78 | anchor = text.replace("/", "_")
79 | ref = f"https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#{anchor}" # noqa: E501
80 |
81 | node = nodes.reference(rawtext, text, refuri=ref, **options)
82 | return [node], []
83 |
84 |
85 | def setup(app: Sphinx):
86 | app.add_css_file("custom.css")
87 | app.add_role("lsp", lsp_role)
88 |
--------------------------------------------------------------------------------
/docs/ext/supported_clients.py:
--------------------------------------------------------------------------------
1 | import importlib.resources as resources
2 |
3 | from docutils import nodes
4 | from docutils.parsers.rst import Directive
5 | from packaging.version import parse as parse_version
6 | from sphinx.application import Sphinx
7 |
8 |
9 | class SupportedClients(Directive):
10 | def load_clients(self):
11 | clients = {}
12 |
13 | for resource in resources.files("pytest_lsp.clients").iterdir():
14 | # Skip the README or any other files that we don't care about.
15 | if not resource.name.endswith(".json"):
16 | continue
17 |
18 | client, version = resource.name.replace(".json", "").split("_v")
19 | client = " ".join([c.capitalize() for c in client.split("_")])
20 |
21 | clients.setdefault(client, []).append(version)
22 |
23 | return clients
24 |
25 | def run(self):
26 | rows = []
27 | clients = self.load_clients()
28 |
29 | for client, versions in clients.items():
30 | version_string = ", ".join(sorted(versions, key=parse_version))
31 | rows.append(
32 | nodes.row(
33 | "",
34 | nodes.entry(
35 | "",
36 | nodes.paragraph("", client),
37 | ),
38 | nodes.entry(
39 | "",
40 | nodes.paragraph("", version_string),
41 | ),
42 | ),
43 | )
44 |
45 | header = nodes.row(
46 | "",
47 | nodes.entry(
48 | "",
49 | nodes.paragraph("", "Client"),
50 | ),
51 | nodes.entry(
52 | "",
53 | nodes.paragraph("", "Versions"),
54 | ),
55 | )
56 |
57 | table = nodes.table(
58 | "",
59 | nodes.tgroup(
60 | "",
61 | nodes.colspec("", colwidth="8"),
62 | nodes.colspec("", colwidth="4"),
63 | nodes.thead("", header),
64 | nodes.tbody("", *rows),
65 | cols=2,
66 | ),
67 | )
68 |
69 | return [table]
70 |
71 |
72 | def setup(app: Sphinx):
73 | app.add_directive("supported-clients", SupportedClients)
74 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | LSP Devtools
2 | ============
3 |
4 | The LSP Devtools project provides a number of tools for making the the
5 | process of developing language servers easier.
6 |
7 | Client Capability Index
8 | -----------------------
9 |
10 | .. important::
11 |
12 | This accuracy of this section entirely depends on the captured capabilities data that is `bundled `__ with pytest-lsp.
13 |
14 | Pull requests for corrections and new data welcome!
15 |
16 | .. toctree::
17 | :glob:
18 | :hidden:
19 | :caption: Client Capabilities
20 |
21 | capabilities/*
22 |
23 | Inspired by `caniuse.com `__ this provides information on which clients support which features of the `LSP Specification `__.
24 |
25 | .. grid:: 2
26 | :gutter: 2
27 |
28 | .. grid-item-card:: General
29 | :columns: 12
30 | :link: /capabilities/general
31 | :link-type: doc
32 | :text-align: center
33 |
34 | General client capabilities.
35 |
36 | .. grid-item-card:: NotebookDocument
37 | :link: /capabilities/notebook-document
38 | :link-type: doc
39 | :text-align: center
40 |
41 | Capabilities for NotebookDocuments.
42 |
43 | .. grid-item-card:: TextDocument
44 | :link: /capabilities/text-document
45 | :link-type: doc
46 | :text-align: center
47 |
48 | Capabilities for text document methods like completion, code actions and more.
49 |
50 | .. grid-item-card:: Window
51 | :link: /capabilities/window
52 | :link-type: doc
53 | :text-align: center
54 |
55 | Work done progress, show document and message requests
56 |
57 | .. grid-item-card:: Workspace
58 | :link: /capabilities/workspace
59 | :link-type: doc
60 | :text-align: center
61 |
62 | File operations, workspace folders and configuration
63 |
64 | lsp-devtools
65 | ------------
66 |
67 | .. toctree::
68 | :hidden:
69 | :caption: lsp-devtools
70 |
71 | lsp-devtools/guide
72 | lsp-devtools/changelog
73 |
74 |
75 | .. figure:: https://user-images.githubusercontent.com/2675694/273293510-e43fdc92-03dd-40c9-aaca-ddb5e526031a.png
76 |
77 | The `lsp-devtools `_ package provides a collection of CLI utilities that help inspect and visualise the interactions between a language client and a server.
78 |
79 | See the :doc:`lsp-devtools/guide/getting-started` guide for details.
80 |
81 | pytest-lsp
82 | ----------
83 |
84 | .. toctree::
85 | :hidden:
86 | :caption: pytest-lsp
87 |
88 | pytest-lsp/guide
89 | pytest-lsp/howto
90 | pytest-lsp/reference
91 | pytest-lsp/changelog
92 |
93 | `pytest-lsp `_ is a pytest plugin for writing end-to-end tests for language servers.
94 |
95 | .. literalinclude:: ./pytest-lsp/guide/window-log-message-output.txt
96 | :language: none
97 |
98 | It works by running the language server in a subprocess and communicating with it over stdio, just like a real language client.
99 | This also means ``pytest-lsp`` can be used to test language servers written in any language - not just Python.
100 |
101 | ``pytest-lsp`` relies on `pygls `__ for its language server protocol implementation.
102 |
103 | See the :doc:`pytest-lsp/guide/getting-started` guide for details on how to write your first test case.
104 |
--------------------------------------------------------------------------------
/docs/lsp-devtools/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ```{include} ../../lib/lsp-devtools/CHANGES.md
4 | :relative-images:
5 | ```
6 |
--------------------------------------------------------------------------------
/docs/lsp-devtools/guide.rst:
--------------------------------------------------------------------------------
1 | User Guide
2 | ----------
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | guide/getting-started
8 | guide/record-command
9 | guide/inspect-command
10 |
--------------------------------------------------------------------------------
/docs/lsp-devtools/guide/getting-started.rst:
--------------------------------------------------------------------------------
1 | Getting Started
2 | ===============
3 |
4 | .. highlight:: none
5 |
6 | This guide will introduce you to the tools available in the ``lsp-devtools`` package.
7 | If you have not done so already, you can install it using ``pipx`` ::
8 |
9 | pipx install lsp-devtools
10 |
11 | .. admonition:: Did you say pipx?
12 |
13 | `pipx `_ is a tool that automates the process of installing Python packages into their own isolated Python environments - useful for standalone applications like ``lsp-devtools``
14 |
15 | The LSP Agent
16 | -------------
17 |
18 | In order to use most of the tools in ``lsp-devtools`` you need to wrap your language server with the LSP Agent.
19 | The agent is a simple program that sits inbetween a language client and the server as shown in the diagram below.
20 |
21 | .. figure:: /images/lsp-devtools-architecture.svg
22 |
23 | ``lsp-devtools`` architecture
24 |
25 | The agent acts as a messenger, forwarding messages from the client to the server and vice versa.
26 | However, it sends an additional copy of each message over a local TCP connection to some "Server" application - typically another ``lsp-devtools`` command like ``record`` or ``tui``.
27 |
28 | In general, using ``lsp-devtools`` can be broken down into a 3 step process.
29 |
30 | #. Configure your language client to launch your language server via the agent, rather than launching it directly.
31 |
32 | #. Start the server application e.g. ``lsp-devtools record`` or ``lsp-devtools tui``
33 |
34 | #. Start your language client.
35 |
36 | .. _lsp-devtools-configure-client:
37 |
38 | Configuring your client
39 | ^^^^^^^^^^^^^^^^^^^^^^^
40 |
41 | In order to wrap your language server with the LSP Agent, you need to be able to modify the command your language client uses to start your language server to the following::
42 |
43 | lsp-devtools agent --
44 |
45 | The ``agent`` command will interpret anything given after the double dashes (``--``) to be the command used to invoke your language server.
46 | By default, the agent will attempt to connect to a server application on ``localhost:8765`` but this can be changed using the ``--host `` and ``--port `` arguments::
47 |
48 | lsp-devtools agent --host 127.0.0.1 --port 1234 --
49 |
50 | .. tip::
51 |
52 | Since the agent only requires your server's start command, you can use ``lsp-devtools`` with a server written in any language.
53 |
54 |
55 | As an example, let's configure Neovim to launch the ``esbonio`` language server via the agent.
56 | Using `nvim-lspconfig `_ a standard configuration might look something like the following
57 |
58 | .. code-block:: lua
59 |
60 | lspconfig.esbonio.setup{
61 | capabilities = capabilities,
62 | cmd = { "esbonio" },
63 | filetypes = {"rst"},
64 | init_options = {
65 | server = {
66 | logLevel = "debug"
67 | },
68 | sphinx = {
69 | buildDir = "${confDir}/_build"
70 | }
71 | },
72 | on_attach = on_attach,
73 | }
74 |
75 | To update this to launch the server via the agent, we need only modify the ``cmd`` field (or add one if it does not exist) to include ``lsp-devtools agent --``
76 |
77 | .. code-block:: diff
78 |
79 | lspconfig.esbonio.setup{
80 | capabilities = capabilities,
81 | - cmd = { "esbonio" },
82 | + cmd = { "lsp-devtools", "agent", "--", "esbonio" },
83 | ...
84 | }
85 |
86 | Server Applications
87 | -------------------
88 |
89 | Once you have your client configured, you need to start the application the agent is going to try to connect to.
90 | Currently ``lsp-devtools`` provides the following applications
91 |
92 | ``lsp-devtools record``
93 | As the name suggests, this command supports recording all (or a subset of) messages in a LSP session to a text file or SQLite database.
94 | However, it can also print these messages direct to the console with support for filtering and custom formatting of message contents.
95 |
96 | .. figure:: /images/record-example.svg
97 |
98 | See :doc:`/lsp-devtools/guide/record-command` for details
99 |
100 | ``lsp-devtools inspect``
101 | An interactive terminal application, powered by `textual `_.
102 |
103 | .. figure:: /images/tui-screenshot.svg
104 |
105 | See :doc:`/lsp-devtools/guide/inspect-command` for details
106 |
--------------------------------------------------------------------------------
/docs/lsp-devtools/guide/inspect-command.rst:
--------------------------------------------------------------------------------
1 | LSP Inspector
2 | =============
3 |
4 | .. figure:: /images/tui-screenshot.svg
5 | :align: center
6 |
7 | The ``lsp-devtools inspect`` command
8 |
9 |
10 | The ``lsp-devtools inspect`` command opens an application that allows you to browse all the messages sent between a client and server during an LSP session.
11 |
--------------------------------------------------------------------------------
/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=.
11 | set BUILDDIR=_build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ```{include} ../../lib/pytest-lsp/CHANGES.md
4 | :relative-images:
5 | ```
6 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/guide.rst:
--------------------------------------------------------------------------------
1 | User Guide
2 | ==========
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | guide/getting-started
8 | guide/language-client
9 | guide/client-capabilities
10 | guide/fixtures
11 | guide/troubleshooting
12 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/guide/client-capabilities-error.txt:
--------------------------------------------------------------------------------
1 | $ pytest -W error::pytest_lsp.LspSpecificationWarning
2 | ======================================== test session starts ========================================
3 | platform linux -- Python 3.11.3, pytest-7.2.0, pluggy-1.0.0
4 | rootdir: test_client_capabilities_error0, configfile: tox.ini
5 | plugins: typeguard-2.13.3, asyncio-0.20.2, lsp-0.2.1
6 | asyncio: mode=Mode.AUTO
7 | collected 1 item
8 |
9 | test_server.py F [100%]
10 |
11 | ============================================= FAILURES ==============================================
12 | _________________________________________ test_completions __________________________________________
13 |
14 | ...
15 | try:
16 | result_checker(capabilities, result)
17 | except AssertionError as e:
18 | > warnings.warn(str(e), LspSpecificationWarning, stacklevel=4)
19 | E pytest_lsp.checks.LspSpecificationWarning: Client does not support snippets.
20 | E assert False
21 |
22 | /.../site-packages/pytest_lsp/checks.py:73: LspSpecificationWarning
23 | ====================================== short test summary info ======================================
24 | FAILED test_server.py::test_completions - pytest_lsp.checks.LspSpecificationWarning: Client does n...
25 | ========================================= 1 failed in 1.16s =========================================
26 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/guide/client-capabilities-ignore.txt:
--------------------------------------------------------------------------------
1 | $ pytest -W ignore::pytest_lsp.LspSpecificationWarning
2 | ======================================== test session starts ========================================
3 | platform linux -- Python 3.11.3, pytest-7.2.0, pluggy-1.0.0
4 | rootdir: test_client_capabilities_ignore0, configfile: tox.ini
5 | plugins: typeguard-2.13.3, asyncio-0.20.2, lsp-0.2.1
6 | asyncio: mode=Mode.AUTO
7 | collected 1 item
8 |
9 | test_server.py . [100%]
10 |
11 | ========================================= 1 passed in 1.02s =========================================
12 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/guide/client-capabilities-output.txt:
--------------------------------------------------------------------------------
1 | $ pytest
2 | ======================================== test session starts ========================================
3 | platform linux -- Python 3.11.3, pytest-7.2.0, pluggy-1.0.0
4 | rootdir: test_client_capabilities0, configfile: tox.ini
5 | plugins: typeguard-2.13.3, asyncio-0.20.2, lsp-0.2.1
6 | asyncio: mode=Mode.AUTO
7 | collected 1 item
8 |
9 | test_server.py . [100%]
10 |
11 | ========================================= warnings summary ==========================================
12 | test_server.py::test_completions
13 | test_client_capabilities0/test_server.py:35: LspSpecificationWarning: Client does not support snippets.
14 | assert False
15 | results = await client.text_document_completion_async(
16 |
17 | -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
18 | =================================== 1 passed, 1 warning in 1.02s ====================================
19 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/guide/client-capabilities.rst:
--------------------------------------------------------------------------------
1 | Client Capabilities
2 | ===================
3 |
4 | The
5 | `initialize `_
6 | request at the start of an LSP session allows the client and server to exchange information about each other.
7 | Of particular interest is the
8 | `ClientCapabilities `_
9 | field which is used to inform the server which parts of the specification the client supports.
10 |
11 | Setting this field to the right value ``pytest-lsp`` can pretend to be a particular editor at a particular version and check to see if the server adapts accordingly.
12 |
13 | .. _pytest-lsp-supported-clients:
14 |
15 | Supported Clients
16 | -----------------
17 |
18 | ``pytest-lsp`` currently supports the following clients and versions.
19 |
20 | .. supported-clients::
21 |
22 | The :func:`~pytest_lsp.client_capabilities` function can be used to load the capabilities corresponding to a given client name
23 |
24 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/client-capabilities/t_server.py
25 | :language: python
26 | :start-at: @pytest_lsp.fixture
27 | :end-at: await lsp_client.shutdown_session()
28 |
29 | .. _pytest-lsp-spec-checks:
30 |
31 | Specification Compliance Checks
32 | -------------------------------
33 |
34 | By setting the client's capabilities to anything other than ``ClientCapabilities()``, ``pytest-lsp`` will automatically enable checks to ensure that the server respects the capabilities published by the client.
35 | If any issues are found, ``pytest-lsp`` will emit an :class:`~pytest_lsp.checks.LspSpecificationWarning`.
36 |
37 | .. tip::
38 |
39 | For full details on the checks that have been implemented see the :mod:`pytest_lsp.checks` module.
40 |
41 |
42 | As an example, let's write a test for the following language server.
43 |
44 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/client-capabilities/server.py
45 | :language: python
46 | :start-at: @server.feature
47 | :end-at: ]
48 |
49 | When it receives a completion request it returns a single item called ``greet`` which, when selected, expands into a snippet making it easier to type the sequence ``"Hello, world!"``.
50 | Let's write a test to confirm it works as expected.
51 |
52 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/client-capabilities/t_server.py
53 | :language: python
54 | :start-at: async def test_completions
55 |
56 | Running this test while pretending to be ``neovim`` we should see that while it passes, ``pytest-lsp`` will emit a warning saying that neovim does not support snippets.
57 |
58 | .. note::
59 |
60 | *Vanilla* Neovim v0.6.1 does not support snippets, though there are many plugins that can be installed to enable support for them.
61 |
62 | .. literalinclude:: ./client-capabilities-output.txt
63 | :language: none
64 |
65 | Strict Checks
66 | ^^^^^^^^^^^^^
67 |
68 | You can upgrade these warnings to be errors if you wish by passing ``-W error::pytest_lsp.LspSpecificationWarning`` to pytest.
69 |
70 | .. literalinclude:: ./client-capabilities-error.txt
71 | :language: none
72 |
73 | Disabling Checks
74 | ^^^^^^^^^^^^^^^^
75 |
76 | Alternatively, you can ignore these warnings by passing ``-W ignore::pytest_lsp.LspSpecificationWarning`` to pytest.
77 |
78 | .. literalinclude:: ./client-capabilities-ignore.txt
79 | :language: none
80 |
81 |
82 | .. seealso::
83 |
84 | :ref:`pytest:controlling-warnings`
85 | Pytest's documentation on configuring how warnings should be handled
86 |
87 | :ref:`python:warning-filter`
88 | Python's built in warning filter syntax
89 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/guide/fixtures.rst:
--------------------------------------------------------------------------------
1 | Fixtures
2 | ========
3 |
4 | .. highlight:: none
5 |
6 | Parameterised Fixtures
7 | ----------------------
8 |
9 | Like regular pytest fixtures, :func:`pytest_lsp.fixture` supports `parameterisation `__.
10 | This can be used to run the same set of tests while pretending to be a different client each time.
11 |
12 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/parameterised-clients/t_server.py
13 | :language: python
14 | :start-at: @pytest_lsp.fixture
15 | :end-at: await lsp_client.shutdown_session()
16 |
17 |
18 | Requesting Other Fixtures
19 | -------------------------
20 |
21 | As you would expect, it's possible to request other fixtures to help set up your client.
22 |
23 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/fixture-passthrough/t_server.py
24 | :language: python
25 | :start-at: @pytest.fixture
26 | :end-at: await lsp_client.shutdown_session()
27 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/guide/getting-started-fail-output.txt:
--------------------------------------------------------------------------------
1 | $ pytest
2 | ============================= test session starts ==============================
3 | platform linux -- Python 3.11.9, pytest-7.4.4, pluggy-1.5.0
4 | rootdir: /tmp/pytest-of-alex/pytest-12/test_getting_started_fail0
5 | configfile: tox.ini
6 | plugins: lsp-0.4.2, asyncio-0.23.8
7 | asyncio: mode=Mode.AUTO
8 | collected 1 item
9 |
10 | test_server.py E [100%]
11 |
12 | ==================================== ERRORS ====================================
13 | ______________________ ERROR at setup of test_completions ______________________
14 |
15 | lsp_client =
16 |
17 | @pytest_lsp.fixture(
18 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
19 | )
20 | async def client(lsp_client: LanguageClient):
21 | # Setup
22 | params = InitializeParams(capabilities=ClientCapabilities())
23 | > await lsp_client.initialize_session(params)
24 |
25 | test_server.py:21:
26 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
27 | /var/home/alex/Projects/swyddfa/lsp-devtools/release/lib/pytest-lsp/pytest_lsp/client.py:245: in initialize_session
28 | response = await self.initialize_async(params)
29 | /var/home/alex/.local/share/hatch/env/virtual/pytest-lsp/oa_H1-lS/hatch-test.py3.11-7/lib/python3.11/site-packages/pygls/lsp/client.py:244: in initialize_async
30 | return await self.protocol.send_request_async("initialize", params)
31 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
32 |
33 | self =
34 | method = 'initialize'
35 | params = InitializeParams(capabilities=ClientCapabilities(workspace=None, text_document=None, notebook_document=None, window=No..., root_path=None, root_uri=None, initialization_options=None, trace=None, work_done_token=None, workspace_folders=None)
36 |
37 | async def send_request_async(self, method, params=None):
38 | """Wrap pygls' ``send_request_async`` implementation. This will
39 |
40 | - Check the params to see if they're compatible with the client's stated
41 | capabilities
42 | - Check the result to see if it's compatible with the client's stated
43 | capabilities
44 |
45 | Parameters
46 | ----------
47 | method
48 | The method name of the request to send
49 |
50 | params
51 | The associated parameters to go with the request
52 |
53 | Returns
54 | -------
55 | Any
56 | The result
57 | """
58 | check_params_against_client_capabilities(
59 | self._server.capabilities, method, params
60 | )
61 | > result = await super().send_request_async(method, params)
62 | E RuntimeError: Server process 42326 exited with code: 0
63 |
64 | /var/home/alex/Projects/swyddfa/lsp-devtools/release/lib/pytest-lsp/pytest_lsp/protocol.py:81: RuntimeError
65 | =========================== short test summary info ============================
66 | ERROR test_server.py::test_completions - RuntimeError: Server process 42326 e...
67 | =============================== 1 error in 1.12s ===============================
68 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/guide/getting-started.rst:
--------------------------------------------------------------------------------
1 | Getting Started
2 | ===============
3 |
4 | .. highlight:: none
5 |
6 | This guide will walk you through the process of writing your first test case using ``pytest-lsp``.
7 |
8 | If you have not done so already, you can install the ``pytest-lsp`` package using pip::
9 |
10 | pip install pytest-lsp
11 |
12 | A Simple Language Server
13 | ------------------------
14 |
15 | Before we can write any tests, we need a language server to test!
16 | For the purposes of this example we'll write a simple language server in Python using the `pygls`_ library but note that ``pytest-lsp`` should work with language servers written in any language or framework.
17 |
18 | The following server implements the ``textDocument/completion`` method
19 |
20 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/getting-started/server.py
21 | :language: python
22 | :end-at: ]
23 |
24 | Copy and paste the above code into a file named ``server.py``.
25 |
26 | A Simple Test Case
27 | ------------------
28 |
29 | Now we can go ahead and test it.
30 | Copy the following code and save it into a file named ``test_server.py``, in the same directory as the ``server.py`` file you created in the previous step.
31 |
32 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/getting-started/t_server.py
33 | :language: python
34 | :end-at: await lsp_client.shutdown_session()
35 |
36 | This creates a `pytest fixture`_ named ``client``, it uses the given ``server_command`` to automatically launch the server in a background process and connect it to a :class:`~pytest_lsp.LanguageClient` instance.
37 |
38 | The setup code (everything before the ``yield``) statement is executed before any tests run, calling :meth:`~pytest_lsp.LanguageClient.initialize_session` on the client to open the LSP session.
39 |
40 | Once all test cases have been called, the code after the ``yield`` statement will be called to shutdown the server and close the session
41 |
42 | With the framework in place, we can go ahead and define our first test case
43 |
44 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/getting-started/t_server.py
45 | :language: python
46 | :start-at: @pytest.mark.asyncio
47 |
48 | All that's left is to run the test suite!
49 |
50 | .. literalinclude:: ./getting-started-fail-output.txt
51 |
52 | We forgot to start the server!
53 | Add the following to the bottom of ``server.py``.
54 |
55 | .. code-block:: python
56 |
57 | if __name__ == "__main__":
58 | server.start_io()
59 |
60 | Let's try again
61 |
62 |
63 | .. code-block:: none
64 |
65 | $ pytest
66 | ================================================ test session starts =================================================
67 | platform linux -- Python 3.11.2, pytest-7.2.0, pluggy-1.0.0
68 | rootdir: /var/home/user/Projects/lsp-devtools/lib/pytest-lsp, configfile: pyproject.toml
69 | plugins: typeguard-2.13.3, asyncio-0.20.2, lsp-0.2.1
70 | asyncio: mode=Mode.AUTO
71 | collected 1 item
72 |
73 | test_server.py . [100%]
74 |
75 | ================================================= 1 passed in 0.96s ==================================================
76 |
77 | Much better!
78 |
79 |
80 | .. _pygls: https://github.com/openlawlibrary/pygls
81 | .. _pytest fixture: https://docs.pytest.org/en/7.1.x/how-to/fixtures.html
82 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/guide/window-log-message-output.txt:
--------------------------------------------------------------------------------
1 | ================================== test session starts ====================================
2 | platform linux -- Python 3.11.2, pytest-7.2.0, pluggy-1.0.0
3 | rootdir: /..., configfile: tox.ini
4 | plugins: typeguard-2.13.3, asyncio-0.20.2, lsp-0.2.1
5 | asyncio: mode=Mode.AUTO
6 | collected 1 item
7 |
8 | test_server.py F [100%]
9 |
10 | ======================================== FAILURES =========================================
11 | ____________________________________ test_completions _____________________________________
12 |
13 | client =
14 | ...
15 | E assert False
16 |
17 | test_server.py:35: AssertionError
18 | ---------------------------- Captured window/logMessages call -----------------------------
19 | LOG: Suggesting item 0
20 | LOG: Suggesting item 1
21 | LOG: Suggesting item 2
22 | LOG: Suggesting item 3
23 | LOG: Suggesting item 4
24 | LOG: Suggesting item 5
25 | LOG: Suggesting item 6
26 | LOG: Suggesting item 7
27 | LOG: Suggesting item 8
28 | LOG: Suggesting item 9
29 | ================================ short test summary info ==================================
30 | FAILED test_server.py::test_completions - assert False
31 | =================================== 1 failed in 1.02s =====================================
32 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/howto.rst:
--------------------------------------------------------------------------------
1 | How To
2 | ======
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | Integrate with lsp-devtools
8 | Migrate to v1
9 | Test Generic JSON-RPC Servers
10 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/howto/integrate-with-lsp-devtools.rst:
--------------------------------------------------------------------------------
1 | How To Integrate ``pytest-lsp`` with ``lsp-devtools``
2 | =====================================================
3 |
4 | ``pytest-lsp`` is able to forward LSP traffic to utilities like :doc:`lsp-devtools record ` and :doc:`lsp-devtools inspect `
5 |
6 | .. important::
7 |
8 | ``pytest-lsp`` does not depend on ``lsp-devtools`` directly and instead assumes that the ``lsp-devtools`` command is available on your ``$PATH``.
9 | It's recommended to install ``lsp-devtools`` via `pipx `__::
10 |
11 | $ pipx install lsp-devtools
12 |
13 | To enable the integration pass the ``--lsp-devtools`` flag to ``pytest``::
14 |
15 | $ pytest --lsp-devtools
16 |
17 | This will make ``pytest-lsp`` send the captured traffic to an ``lsp-devtools`` command listening on ``localhost:8765`` by default.
18 |
19 | To change the default host and/or port number you can pass it to the ``--lsp-devtools`` cli option::
20 |
21 | $ pytest --lsp-devtools 1234 # change port number
22 | $ pytest --lsp-devtools 127.0.01:1234 # change host and port
23 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/howto/migrate-to-v1.rst:
--------------------------------------------------------------------------------
1 | How To Migrate to v1
2 | ====================
3 |
4 | The ``v1`` release of ``pytest-lsp`` contains some breaking changes, mostly as a result of changes in the wider ecosystem.
5 | This guide summarises the changes and provides references on where to get more details.
6 |
7 | Python Support
8 | --------------
9 |
10 | This release removes support for Python 3.8 and adds support for Python 3.13.
11 |
12 | ``pytest``
13 | ----------
14 |
15 | This release removes support for pytest ``v7``, if you have not done so already please update to pytest ``v8``.
16 |
17 |
18 | ``pytest-asyncio``
19 | ------------------
20 |
21 | The minimum required version for ``pytest-asyncio`` is now ``0.24``, see `this guide `__ for details on upgrading
22 |
23 | ``pygls``
24 | ---------
25 |
26 | ``pygls``, the underlying language server protocol implementation used by ``pytest-lsp`` has been upgraded to ``v2``.
27 | See `this guide `__ for details on the breaking changes this brings.
28 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/howto/testing-json-rpc-servers.rst:
--------------------------------------------------------------------------------
1 | How To Test Generic JSON-RPC Servers
2 | ====================================
3 |
4 | While ``pytest-lsp`` is primarily focused on writing tests for LSP servers it is possible to reuse some of the machinery to test other JSON-RPC servers.
5 |
6 | A Simple JSON-RPC Server
7 | ------------------------
8 |
9 | As an example we'll reuse some of the `pygls`_ internals to write a simple JSON-RPC server that implements the following protocol.
10 |
11 | - client to server request ``math/add``, returns the sum of two numbers ``a`` and ``b``
12 | - client to server request ``math/sub``, returns the difference of two numbers ``a`` and ``b``
13 | - client to server notification ``server/exit`` that instructs the server to exit
14 | - server to client notification ``log/message``, allows the server to send debug messages to the client.
15 |
16 | .. note::
17 |
18 | The details of the implementation below don't really matter as we just need *something* to help us illustrate how to use ``pytest-lsp`` in this way.
19 |
20 | Remember you can write your servers in whatever language/framework you prefer!
21 |
22 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/generic-rpc/server.py
23 | :language: python
24 |
25 | Constructing a Client
26 | ---------------------
27 |
28 | While ``pytest-lsp`` can manage the connection between client and server, it needs to be given a client that understands the protocol that the server implements.
29 | This is done with a factory function
30 |
31 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/generic-rpc/t_server.py
32 | :language: python
33 | :start-at: def client_factory():
34 | :end-at: return client
35 |
36 | The Client Fixture
37 | ------------------
38 |
39 | Once you have your factory function defined you can pass it to the :class:`~pytest_lsp.ClientServerConfig` when defining your client fixture
40 |
41 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/generic-rpc/t_server.py
42 | :language: python
43 | :start-at: @pytest_lsp.fixture(
44 | :end-at: rpc_client.protocol.notify
45 |
46 | Writing Test Cases
47 | ------------------
48 |
49 | With the client fixuture defined, test cases are written almost identically as they would be for your LSP servers.
50 | The only difference is that the generic :meth:`~pygls:pygls.protocol.JsonRPCProtocol.send_request_async` and :meth:`~pygls:pygls.protocol.JsonRPCProtocol.notify` methods are used to communicate with the server.
51 |
52 | .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/generic-rpc/t_server.py
53 | :language: python
54 | :start-at: @pytest.mark.asyncio
55 |
56 | However, it is also possible to extend the base :class:`~pygls:pygls.client.JsonRPCClient` to provide a higher level interface to your server.
57 | See the `SubprocessSphinxClient`_ from the `esbonio`_ project for such an example.
58 |
59 | .. _esbonio: https://github.com/swyddfa/esbonio
60 | .. _pygls: https://github.com/openlawlibrary/pygls
61 | .. _SubprocessSphinxClient: https://github.com/swyddfa/esbonio/blob/develop/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py
62 |
--------------------------------------------------------------------------------
/docs/pytest-lsp/reference.rst:
--------------------------------------------------------------------------------
1 | API Reference
2 | =============
3 |
4 | .. currentmodule:: pytest_lsp
5 |
6 | LanguageClient
7 | --------------
8 |
9 | .. autoclass:: LanguageClient
10 | :members:
11 | :show-inheritance:
12 |
13 |
14 | Test Setup
15 | ----------
16 |
17 | .. autofunction:: fixture
18 |
19 | .. autofunction:: client_capabilities
20 |
21 | .. autoclass:: ClientServerConfig
22 | :members:
23 |
24 | .. autofunction:: make_test_lsp_client
25 |
26 |
27 | Checks
28 | ------
29 |
30 | .. automodule:: pytest_lsp.checks
31 | :members:
32 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | # This assumes you are running the pip install command from the root of the repo e.g.
2 | # $ pip install -r docs/requirements.txt
3 | sphinx
4 | sphinx-copybutton
5 | sphinx-design
6 | furo
7 | myst-parser
8 | pygls>=1.1.0
9 | -e lib/pytest-lsp
10 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "nixpkgs": {
4 | "locked": {
5 | "lastModified": 1688585123,
6 | "narHash": "sha256-+xFOB4WaRUHuZI7H1tWHTrwY4BnbPmh8M1n/XhPRH0w=",
7 | "owner": "NixOS",
8 | "repo": "nixpkgs",
9 | "rev": "23de9f3b56e72632c628d92b71c47032e14a3d4d",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "NixOS",
14 | "ref": "nixpkgs-unstable",
15 | "repo": "nixpkgs",
16 | "type": "github"
17 | }
18 | },
19 | "root": {
20 | "inputs": {
21 | "nixpkgs": "nixpkgs",
22 | "utils": "utils"
23 | }
24 | },
25 | "systems": {
26 | "locked": {
27 | "lastModified": 1681028828,
28 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
29 | "owner": "nix-systems",
30 | "repo": "default",
31 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
32 | "type": "github"
33 | },
34 | "original": {
35 | "owner": "nix-systems",
36 | "repo": "default",
37 | "type": "github"
38 | }
39 | },
40 | "utils": {
41 | "inputs": {
42 | "systems": "systems"
43 | },
44 | "locked": {
45 | "lastModified": 1687709756,
46 | "narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
47 | "owner": "numtide",
48 | "repo": "flake-utils",
49 | "rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "numtide",
54 | "repo": "flake-utils",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Developer tooling for language servers";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6 | utils.url = "github:numtide/flake-utils";
7 | };
8 |
9 | outputs = { self, nixpkgs, utils }:
10 | {
11 | overlays.default = self: super:
12 | nixpkgs.lib.composeManyExtensions [
13 | (import ./lib/pytest-lsp/nix/pytest-lsp-overlay.nix)
14 | (import ./lib/lsp-devtools/nix/lsp-devtools-overlay.nix)
15 | ] self super;
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 0.2.0
3 | commit = False
4 | tag = False
5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(.dev(?P\d+))?
6 | serialize =
7 | {major}.{minor}.{patch}.dev{dev}
8 | {major}.{minor}.{patch}
9 |
10 | [bumpversion:file:lsp_devtools/__init__.py]
11 |
12 | [bumpversion:file:pyproject.toml]
13 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/.gitignore:
--------------------------------------------------------------------------------
1 | result
2 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Alex Carney
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include lsp_devtools/handlers/dbinit.sql
2 | include lsp_devtools/py.typed
3 | include lsp_devtools/tui/app.css
4 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/README.md:
--------------------------------------------------------------------------------
1 | # lsp-devtools: Developer tooling for language servers
2 |
3 | This package provides a collection of cli utilities to support the development of language servers.
4 | While this is a Python package, it can be used with language servers written in any langauge.
5 |
6 | 
7 |
8 | Available commands:
9 |
10 | - `agent`: Wrap a language server allowing other commands to access the messages sent to and from the client
11 | - `client`: **Experimental** a language client with an embedded inspector. Powered by [textual](https://textual.textualize.io/)
12 | - `record`: Connects to an agent and record traffic to file, sqlite db or console. Supports filtering and formatting the output
13 | - `inspect`: A text user interface to visualise and inspect LSP traffic. Powered by [textual](https://textual.textualize.io/)
14 |
15 | See the [documentation](https://lsp-devtools.readthedocs.io/en/latest/) for more information
16 |
17 | ## Installation
18 |
19 | Install using [pipx](https://pypa.github.io/pipx/)
20 |
21 | ```
22 | pipx install lsp-devtools
23 | ```
24 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/changes/github-template.html:
--------------------------------------------------------------------------------
1 | %(body_pre_docinfo)s
2 | %(docinfo)s
3 | %(body)s
4 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "nixpkgs": {
4 | "locked": {
5 | "lastModified": 1704161960,
6 | "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=",
7 | "owner": "NixOS",
8 | "repo": "nixpkgs",
9 | "rev": "63143ac2c9186be6d9da6035fa22620018c85932",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "NixOS",
14 | "ref": "nixpkgs-unstable",
15 | "repo": "nixpkgs",
16 | "type": "github"
17 | }
18 | },
19 | "root": {
20 | "inputs": {
21 | "nixpkgs": "nixpkgs",
22 | "utils": "utils"
23 | }
24 | },
25 | "systems": {
26 | "locked": {
27 | "lastModified": 1681028828,
28 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
29 | "owner": "nix-systems",
30 | "repo": "default",
31 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
32 | "type": "github"
33 | },
34 | "original": {
35 | "owner": "nix-systems",
36 | "repo": "default",
37 | "type": "github"
38 | }
39 | },
40 | "utils": {
41 | "inputs": {
42 | "systems": "systems"
43 | },
44 | "locked": {
45 | "lastModified": 1701680307,
46 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
47 | "owner": "numtide",
48 | "repo": "flake-utils",
49 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "numtide",
54 | "repo": "flake-utils",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "lsp-devtools: Developer tooling for language servers";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6 | utils.url = "github:numtide/flake-utils";
7 | };
8 |
9 | outputs = { self, nixpkgs, utils }:
10 |
11 | let
12 | eachPythonVersion = versions: f:
13 | builtins.listToAttrs (builtins.map (version: {name = "py${version}"; value = f version; }) versions); in {
14 |
15 | overlays.default = import ./nix/lsp-devtools-overlay.nix;
16 |
17 | packages = utils.lib.eachDefaultSystemMap (system:
18 | let
19 | pkgs = import nixpkgs { inherit system; overlays = [ self.overlays.default ]; };
20 | in
21 | eachPythonVersion [ "38" "39" "310" "311"] (pyVersion:
22 | pkgs."python${pyVersion}Packages".lsp-devtools
23 | )
24 | );
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/hatch.toml:
--------------------------------------------------------------------------------
1 | [version]
2 | path = "lsp_devtools/__init__.py"
3 | validate-bump = false
4 |
5 | [build.targets.sdist]
6 | include = ["lsp_devtools", "tests", "CHANGES.md"]
7 |
8 | [build.targets.wheel]
9 | packages = ["lsp_devtools"]
10 |
11 | [envs.hatch-test]
12 | extra-dependencies = ["pytest-asyncio"]
13 |
14 | [envs.hatch-test.env-vars]
15 | UV_PRERELEASE="allow"
16 |
17 | [envs.hatch-static-analysis]
18 | config-path = "ruff_defaults.toml"
19 | dependencies = ["ruff==0.8.0"]
20 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.2.4"
2 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from lsp_devtools.cli import main
4 |
5 | if __name__ == "__main__":
6 | sys.exit(main())
7 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/agent/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import argparse
4 | import asyncio
5 | import subprocess
6 | import sys
7 |
8 | from .agent import Agent
9 | from .agent import RPCMessage
10 | from .agent import logger
11 | from .agent import parse_rpc_message
12 | from .client import AgentClient
13 | from .server import AgentServer
14 |
15 | __all__ = [
16 | "Agent",
17 | "AgentClient",
18 | "AgentServer",
19 | "RPCMessage",
20 | "logger",
21 | "parse_rpc_message",
22 | ]
23 |
24 |
25 | async def forward_stderr(server: asyncio.subprocess.Process):
26 | """Forward the server's stderr to the agent's stderr."""
27 | if server.stderr is None:
28 | return
29 |
30 | # EOF is signalled with an empty bytestring
31 | while (line := await server.stderr.readline()) != b"":
32 | sys.stderr.buffer.write(line)
33 |
34 |
35 | async def main(args, extra: list[str]):
36 | if extra is None:
37 | print("Missing server start command", file=sys.stderr)
38 | return 1
39 |
40 | command, *arguments = extra
41 | server = await asyncio.create_subprocess_exec(
42 | command,
43 | *arguments,
44 | stdin=subprocess.PIPE,
45 | stdout=subprocess.PIPE,
46 | stderr=subprocess.PIPE,
47 | )
48 | client = AgentClient()
49 | agent = Agent(server, sys.stdin.buffer, sys.stdout.buffer, client.forward_message)
50 |
51 | await asyncio.gather(
52 | client.start_tcp(args.host, args.port),
53 | agent.start(),
54 | forward_stderr(server),
55 | )
56 |
57 |
58 | def run_agent(args, extra: list[str]):
59 | try:
60 | asyncio.run(main(args, extra))
61 | except asyncio.CancelledError:
62 | pass
63 |
64 |
65 | def cli(commands: argparse._SubParsersAction):
66 | cmd: argparse.ArgumentParser = commands.add_parser(
67 | "agent",
68 | help="instrument an LSP session",
69 | formatter_class=argparse.RawDescriptionHelpFormatter,
70 | description="""\
71 | This command runs the given JSON-RPC server as a subprocess, wrapping it in a
72 | an "AgentClient" which will capture all messages sent to/from the wrapped
73 | server, forwarding them onto an "AgentServer" to be processed.
74 |
75 | To wrap a server, supply its start command after all other agent options and
76 | preceeded by a `--`, for example:
77 |
78 | lsp-devtools agent -p 1234 -- python -m esbonio
79 |
80 | Wrapping a JSON-RPC server with this command is required to enable the
81 | majority of the lsp-devtools suite of tools.
82 |
83 | ┌─ RPC Client ─┐ ┌──── Agent Client ────┐ ┌─ RPC Server ─┐
84 | │ │ │ ┌──────────────┐ │ │ │
85 | │ stdout│─────│───│ │───│────│stdin │
86 | │ │ │ │ Agent │ │ │ │
87 | │ stdin│─────│───│ │───│────│stdout │
88 | │ │ │ └──────────────┘ │ │ │
89 | │ │ │ │ │ │
90 | └──────────────┘ └──────────────────────┘ └──────────────┘
91 | │
92 | │ tcp/websocket
93 | │
94 | ┌──────────────┐
95 | │ │
96 | │ Agent Server │
97 | │ │
98 | └──────────────┘
99 |
100 | """,
101 | )
102 |
103 | cmd.add_argument(
104 | "--host",
105 | help="the host to connect to.",
106 | default="localhost",
107 | )
108 | cmd.add_argument(
109 | "-p",
110 | "--port",
111 | help="the port to connect to",
112 | default=8765,
113 | )
114 |
115 | cmd.set_defaults(run=run_agent)
116 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/agent/client.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 | import inspect
5 | import typing
6 |
7 | import stamina
8 | from pygls.client import JsonRPCClient
9 | from pygls.protocol import default_converter
10 |
11 | from lsp_devtools.agent.protocol import AgentProtocol
12 |
13 | if typing.TYPE_CHECKING:
14 | from typing import Any
15 |
16 |
17 | class AgentClient(JsonRPCClient):
18 | """Client for connecting to an AgentServer instance."""
19 |
20 | protocol: AgentProtocol
21 |
22 | def __init__(self):
23 | super().__init__(
24 | protocol_cls=AgentProtocol, converter_factory=default_converter
25 | )
26 | self.connected = False
27 | self._buffer: list[bytes] = []
28 | self._tasks: set[asyncio.Task[Any]] = set()
29 |
30 | def _report_server_error(self, error, source):
31 | # Bail on error
32 | # TODO: Report the actual error somehow
33 | self._stop_event.set()
34 |
35 | def feature(self, feature_name: str, options: Any | None = None):
36 | return self.protocol.fm.feature(feature_name, options)
37 |
38 | async def start_tcp(self, host: str, port: int):
39 | # The user might not have started the server app immediately and since the
40 | # agent will live as long as the wrapper language server we may as well
41 | # try indefinitely.
42 | retries = stamina.retry_context(
43 | on=OSError,
44 | attempts=None,
45 | timeout=None,
46 | wait_initial=1,
47 | wait_max=60,
48 | )
49 | async for attempt in retries:
50 | with attempt:
51 | await super().start_tcp(host, port)
52 | self.connected = True
53 |
54 | def forward_message(self, message: bytes):
55 | """Forward the given message to the server instance."""
56 |
57 | if not self.connected or self.protocol.writer is None:
58 | self._buffer.append(message)
59 | return
60 |
61 | # Send any buffered messages
62 | while len(self._buffer) > 0:
63 | res = self.protocol.writer.write(self._buffer.pop(0))
64 | if inspect.isawaitable(res):
65 | task = asyncio.ensure_future(res)
66 | task.add_done_callback(self._tasks.discard)
67 | self._tasks.add(task)
68 |
69 | res = self.protocol.writer.write(message)
70 | if inspect.isawaitable(res):
71 | task = asyncio.ensure_future(res)
72 | task.add_done_callback(self._tasks.discard)
73 | self._tasks.add(task)
74 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/agent/protocol.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pygls.protocol import JsonRPCProtocol
4 |
5 |
6 | class AgentProtocol(JsonRPCProtocol):
7 | """The RPC protocol exposed by the agent."""
8 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/agent/server.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 | import json
5 | import logging
6 | import traceback
7 | import typing
8 |
9 | from pygls.protocol import default_converter
10 | from pygls.server import JsonRPCServer
11 |
12 | from lsp_devtools.agent.agent import aio_readline
13 | from lsp_devtools.agent.protocol import AgentProtocol
14 | from lsp_devtools.database import Database
15 |
16 | if typing.TYPE_CHECKING:
17 | from typing import Any
18 |
19 | from lsp_devtools.agent.agent import MessageHandler
20 |
21 |
22 | class AgentServer(JsonRPCServer):
23 | """A pygls server that accepts connections from agents allowing them to send their
24 | collected messages."""
25 |
26 | lsp: AgentProtocol
27 |
28 | def __init__(
29 | self,
30 | *args,
31 | logger: logging.Logger | None = None,
32 | handler: MessageHandler | None = None,
33 | **kwargs,
34 | ):
35 | if "protocol_cls" not in kwargs:
36 | kwargs["protocol_cls"] = AgentProtocol
37 |
38 | if "converter_factory" not in kwargs:
39 | kwargs["converter_factory"] = default_converter
40 |
41 | super().__init__(*args, **kwargs)
42 |
43 | self.logger = logger or logging.getLogger(__name__)
44 | self.handler = handler or self._default_handler
45 | self.db: Database | None = None
46 |
47 | self._client_buffer: list[str] = []
48 | self._server_buffer: list[str] = []
49 | self._tcp_server: asyncio.Task | None = None
50 |
51 | def _default_handler(self, data: bytes):
52 | message = self.protocol.structure_message(json.loads(data))
53 | self.protocol.handle_message(message)
54 |
55 | def _report_server_error(self, error: Exception, source):
56 | """Report internal server errors."""
57 | tb = "".join(
58 | traceback.format_exception(type(error), error, error.__traceback__)
59 | )
60 | self.logger.error("%s: %s", type(error).__name__, error)
61 | self.logger.debug("%s", tb)
62 |
63 | def feature(self, feature_name: str, options: Any | None = None):
64 | return self.lsp.fm.feature(feature_name, options)
65 |
66 | async def start_tcp(self, host: str, port: int) -> None: # type: ignore[override]
67 | async def handle_client(
68 | reader: asyncio.StreamReader, writer: asyncio.StreamWriter
69 | ):
70 | self.protocol.set_writer(writer)
71 |
72 | try:
73 | await aio_readline(reader, self.handler)
74 | except asyncio.CancelledError:
75 | pass
76 | finally:
77 | writer.close()
78 | await writer.wait_closed()
79 |
80 | # Uncomment if we ever need to introduce a mode where the server stops
81 | # automatically once a session ends.
82 | #
83 | # self.stop()
84 |
85 | server = await asyncio.start_server(handle_client, host, port)
86 | async with server:
87 | self._tcp_server = asyncio.create_task(server.serve_forever())
88 | await self._tcp_server
89 |
90 | def stop(self):
91 | if self._tcp_server is not None:
92 | self._tcp_server.cancel()
93 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/cli.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import importlib
3 | import logging
4 | import sys
5 | import traceback
6 |
7 | from lsp_devtools import __version__
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | BUILTIN_COMMANDS = [
13 | "lsp_devtools.agent",
14 | "lsp_devtools.client",
15 | "lsp_devtools.inspector",
16 | "lsp_devtools.record",
17 | ]
18 |
19 |
20 | def load_command(commands: argparse._SubParsersAction, name: str):
21 | try:
22 | mod = importlib.import_module(name)
23 | except Exception:
24 | logger.warning("Unable to load command '%s'\n%s", name, traceback.format_exc())
25 | return
26 |
27 | if not hasattr(mod, "cli"):
28 | logger.warning("Unable to load command '%s': missing 'cli' definition", name)
29 | return
30 |
31 | try:
32 | mod.cli(commands)
33 | except Exception:
34 | logger.warning("Unable to load command '%s'\n%s", name, traceback.format_exc())
35 | return
36 |
37 |
38 | def main():
39 | cli = argparse.ArgumentParser(
40 | prog="lsp-devtools", description="Developer tooling for language servers"
41 | )
42 | cli.add_argument("--version", action="version", version=f"%(prog)s v{__version__}")
43 | commands = cli.add_subparsers(title="commands")
44 |
45 | for mod in BUILTIN_COMMANDS:
46 | load_command(commands, mod)
47 |
48 | try:
49 | idx = sys.argv.index("--")
50 | args, extra = sys.argv[1:idx], sys.argv[idx + 1 :]
51 | except ValueError:
52 | args, extra = sys.argv[1:], None
53 |
54 | parsed_args = cli.parse_args(args)
55 |
56 | if hasattr(parsed_args, "run"):
57 | return parsed_args.run(parsed_args, extra)
58 |
59 | cli.print_help()
60 | return 0
61 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/client/app.css:
--------------------------------------------------------------------------------
1 | Screen {
2 | layers: base overlay;
3 | }
4 |
5 | Explorer {
6 | width: 30;
7 | dock: left;
8 | transition: offset 300ms out_cubic;
9 | }
10 | Explorer.-hidden {
11 | display: none;
12 | }
13 |
14 | Header {
15 | dock: top;
16 | }
17 |
18 | Devtools {
19 | width: 40%;
20 | dock: right;
21 | transition: offset 300ms out_cubic;
22 | }
23 | Devtools.-hidden {
24 | display: none;
25 | }
26 |
27 | MessagesTable {
28 | height: 100%;
29 | }
30 |
31 | CompletionList {
32 | height: 10;
33 | width: 30;
34 | layer: overlay;
35 | }
36 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/client/editor/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import pathlib
4 | import typing
5 |
6 | from lsprotocol import types
7 | from textual import on
8 | from textual.app import ComposeResult
9 | from textual.containers import Vertical
10 | from textual.widgets import OptionList
11 |
12 | from .completion import CompletionList
13 | from .text_editor import TextEditor
14 |
15 | if typing.TYPE_CHECKING:
16 | from lsp_devtools.client.lsp import LanguageClient
17 |
18 |
19 | class EditorView(Vertical):
20 | """A container to manage all the widgets that make up a single text editor."""
21 |
22 | def __init__(self, lsp_client: LanguageClient, *args, **kwargs):
23 | super().__init__(*args, **kwargs)
24 | self.lsp_client = lsp_client
25 |
26 | def compose(self) -> ComposeResult:
27 | yield TextEditor(self.lsp_client)
28 |
29 | def open_file(self, path: pathlib.Path):
30 | editor = self.query_one(TextEditor)
31 | editor.open_file(path)
32 | editor.focus()
33 |
34 | def on_text_editor_completion(self, completion: TextEditor.Completion):
35 | """Render textDocument/completion results."""
36 | candidates = CompletionList.fromresult(completion.result)
37 | if candidates is None:
38 | return
39 |
40 | editor = self.query_one(TextEditor)
41 | row, col = editor.cursor_location
42 |
43 | gutter_width = 2 # TODO: How to get actual gutter width?
44 | first_line = 0 # TODO: How to get the first visible line number?
45 | candidates.offset = (col + gutter_width, row - first_line + 1)
46 |
47 | self.mount(candidates)
48 | self.app.set_focus(candidates)
49 |
50 | @on(OptionList.OptionSelected)
51 | def insert_selected_completion(self, event: CompletionList.OptionSelected):
52 | """Insert the completion item selected by the user into the editor."""
53 | selected: types.CompletionItem = event.option.prompt # type: ignore
54 | event.option_list.action_dismiss() # type: ignore
55 |
56 | editor = self.query_one(TextEditor)
57 | if (edit := selected.text_edit) is not None:
58 | # TODO: Support InsertReplaceEdit
59 | if isinstance(edit, types.InsertReplaceEdit):
60 | return
61 |
62 | # TextEdit support.
63 | start = edit.range.start.line, edit.range.start.character
64 | end = edit.range.end.line, edit.range.end.character
65 |
66 | with editor.set_state(suppress_completion=True):
67 | editor.replace(
68 | edit.new_text, start, end, maintain_selection_offset=False
69 | )
70 |
71 | # TODO: Support insert_text
72 | # TODO: Fallback to label
73 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/client/editor/completion.py:
--------------------------------------------------------------------------------
1 | from lsprotocol import types
2 | from textual import events
3 | from textual.binding import Binding
4 | from textual.widgets import OptionList
5 |
6 |
7 | class CompletionList(OptionList):
8 | BINDINGS = [
9 | Binding("escape", "dismiss", "Dismiss", show=False),
10 | ]
11 |
12 | @classmethod
13 | def fromresult(cls, result):
14 | """Build a list of completion candidates based on a response from the
15 | language server."""
16 |
17 | if result is None:
18 | return None
19 |
20 | if isinstance(result, types.CompletionList):
21 | items = result.items
22 | else:
23 | items = result
24 |
25 | if len(items) == 0:
26 | return None
27 |
28 | candidates = cls()
29 | candidates.add_options(
30 | sorted(
31 | [CompletionItem(i) for i in items],
32 | key=lambda i: i.item.label, # type: ignore
33 | )
34 | )
35 | return candidates
36 |
37 | def on_blur(self, event: events.Blur):
38 | self.action_dismiss()
39 |
40 | def action_dismiss(self):
41 | self.remove()
42 | if self.parent:
43 | self.app.set_focus(self.parent) # type: ignore
44 |
45 |
46 | class CompletionItem:
47 | """Renders a completion item for display in a completion list."""
48 |
49 | def __init__(self, item: types.CompletionItem):
50 | self.item = item
51 |
52 | def __rich__(self):
53 | # TODO: Make pretty
54 | return self.item.label
55 |
56 | def __getattr__(self, key):
57 | return getattr(self.item, key)
58 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/client/editor/text_editor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import contextlib
4 | import pathlib
5 | import typing
6 | from typing import Union
7 |
8 | from lsprotocol import types
9 | from pygls import uris as uri
10 | from pygls.capabilities import get_capability
11 | from textual.message import Message
12 | from textual.widgets import TextArea
13 |
14 | if typing.TYPE_CHECKING:
15 | from lsp_devtools.client.lsp import LanguageClient
16 |
17 | CompletionResult = Union[list[types.CompletionItem], types.CompletionList, None]
18 |
19 |
20 | # TODO: Refactor to
21 | # - emit relevent events.
22 | # - split handlers out into multiple features that can listen and respond
23 | # to these events..
24 | class TextEditor(TextArea):
25 | """A wrapper around textual's ``TextArea`` widget."""
26 |
27 | class Completion(Message):
28 | """Emitted when completion results are received."""
29 |
30 | def __init__(self, result: CompletionResult):
31 | self.result = result
32 | super().__init__()
33 |
34 | def __init__(self, lsp_client: LanguageClient, *args, **kwargs):
35 | super().__init__(*args, **kwargs)
36 | self.uri = None
37 | self.version = 0
38 |
39 | self.lsp_client = lsp_client
40 | self.suppress_completion = False
41 |
42 | @contextlib.contextmanager
43 | def set_state(self, **kwargs):
44 | """Temporarily override a value on the editor."""
45 | old_values = {}
46 | for key, value in kwargs.items():
47 | old_values[key] = getattr(self, key)
48 | setattr(self, key, value)
49 |
50 | yield
51 |
52 | for key, value in old_values.items():
53 | setattr(self, key, value)
54 |
55 | @property
56 | def completion_triggers(self):
57 | """Return the completion trigger characters registered by the server."""
58 | return get_capability(
59 | self.lsp_client.server_capabilities, # type: ignore
60 | "completion_provider.trigger_characters",
61 | set(),
62 | )
63 |
64 | def open_file(self, path: pathlib.Path):
65 | self.uri = uri.from_fs_path(str(path.resolve()))
66 | if self.uri is None:
67 | return
68 |
69 | content = path.read_text()
70 | self.version = 0
71 | self.load_text(content)
72 |
73 | self.lsp_client.text_document_did_open(
74 | types.DidOpenTextDocumentParams(
75 | text_document=types.TextDocumentItem(
76 | uri=self.uri,
77 | language_id="restructuredtext",
78 | version=self.version,
79 | text=content,
80 | )
81 | )
82 | )
83 |
84 | def edit(self, edit):
85 | """Extend the base ``edit()`` method to.
86 |
87 | - Ensure that any edits that are made to the document are syncronised with the
88 | server.
89 | - Completions are triggered if necessary.
90 | """
91 | super().edit(edit)
92 |
93 | if self.uri is None:
94 | return
95 |
96 | self.version += 1
97 | start_line, start_col = edit.from_location
98 | end_line, end_col = edit.to_location
99 |
100 | self.lsp_client.text_document_did_change(
101 | types.DidChangeTextDocumentParams(
102 | text_document=types.VersionedTextDocumentIdentifier(
103 | version=self.version, uri=self.uri
104 | ),
105 | content_changes=[
106 | types.TextDocumentContentChangePartial(
107 | text=edit.text,
108 | range=types.Range(
109 | start=types.Position(line=start_line, character=start_col),
110 | end=types.Position(line=end_line, character=end_col),
111 | ),
112 | )
113 | ],
114 | )
115 | )
116 |
117 | if len(edit.text) == 0:
118 | return
119 |
120 | char = edit.text[-1]
121 | if not self.suppress_completion and char in self.completion_triggers:
122 | # TODO: How to send $/cancelRequest if a worker is cancelled?
123 | self.run_worker(
124 | self.trigger_completion(end_line, end_col),
125 | group="lsp-completion",
126 | exclusive=True,
127 | )
128 |
129 | async def trigger_completion(self, line: int, character: int):
130 | """Trigger completion at the given location."""
131 |
132 | if self.uri is None:
133 | return
134 |
135 | result = await self.lsp_client.text_document_completion_async(
136 | types.CompletionParams(
137 | text_document=types.TextDocumentIdentifier(uri=self.uri),
138 | position=types.Position(line=line, character=character),
139 | )
140 | )
141 |
142 | self.post_message(self.Completion(result))
143 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/client/lsp.py:
--------------------------------------------------------------------------------
1 | import importlib.metadata
2 | import json
3 | from datetime import datetime
4 | from datetime import timezone
5 | from typing import Optional
6 | from uuid import uuid4
7 |
8 | from lsprotocol import types
9 | from pygls.lsp.client import BaseLanguageClient
10 | from pygls.protocol import LanguageServerProtocol
11 |
12 | from lsp_devtools.agent import logger
13 |
14 | UTC = timezone.utc
15 | VERSION = importlib.metadata.version("lsp-devtools")
16 |
17 |
18 | class RecordingLSProtocol(LanguageServerProtocol):
19 | """A version of the LanguageServerProtocol that also records all the traffic."""
20 |
21 | def __init__(self, server, converter):
22 | super().__init__(server, converter)
23 | self.session_id = ""
24 |
25 | def _procedure_handler(self, message):
26 | logger.info(
27 | "%s",
28 | json.dumps(message, default=self._serialize_message),
29 | extra={
30 | "Message-Source": "server",
31 | "Message-Session": self.session_id,
32 | "Message-Timestamp": datetime.now(tz=UTC).isoformat(),
33 | },
34 | )
35 | return super()._procedure_handler(message)
36 |
37 | def _send_data(self, data):
38 | logger.info(
39 | "%s",
40 | json.dumps(data, default=self._serialize_message),
41 | extra={
42 | "Message-Source": "client",
43 | "Message-Session": self.session_id,
44 | "Message-Timestamp": datetime.now(tz=UTC).isoformat(),
45 | },
46 | )
47 | return super()._send_data(data)
48 |
49 |
50 | class LanguageClient(BaseLanguageClient):
51 | """A language client for integrating with a textual text edit."""
52 |
53 | def __init__(self):
54 | super().__init__("lsp-devtools", VERSION, protocol_cls=RecordingLSProtocol)
55 |
56 | self.session_id = str(uuid4())
57 | self.protocol.session_id = self.session_id # type: ignore[attr-defined]
58 | self._server_capabilities: Optional[types.ServerCapabilities] = None
59 |
60 | @property
61 | def server_capabilities(self) -> types.ServerCapabilities:
62 | if self._server_capabilities is None:
63 | raise RuntimeError(
64 | "sever_capabilities is None - has the server been initialized?"
65 | )
66 |
67 | return self._server_capabilities
68 |
69 | async def initialize_async(
70 | self, params: types.InitializeParams
71 | ) -> types.InitializeResult:
72 | result = await super().initialize_async(params)
73 | self._server_capabilities = result.capabilities
74 |
75 | return result
76 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/handlers/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import json
4 | import logging
5 | import typing
6 | from datetime import datetime
7 |
8 | import attrs
9 |
10 | if typing.TYPE_CHECKING:
11 | from collections.abc import Mapping
12 | from typing import Any
13 | from typing import Literal
14 |
15 | MessageSource = Literal["client", "server"]
16 |
17 |
18 | def maybe_json(value):
19 | try:
20 | return json.loads(value)
21 | except Exception:
22 | return value
23 |
24 |
25 | @attrs.define
26 | class LspMessage:
27 | """A container that holds a message from the LSP protocol, with some additional
28 | metadata."""
29 |
30 | session: str
31 | """An id representing the session the message is a part of."""
32 |
33 | timestamp: datetime
34 | """When the message was sent."""
35 |
36 | source: MessageSource
37 | """Indicates if the message was sent by the client or the server."""
38 |
39 | id: str | None
40 | """The ``id`` field, if it exists."""
41 |
42 | method: str | None
43 | """The ``method`` field, if it exists."""
44 |
45 | params: Any | None = attrs.field(converter=maybe_json)
46 | """The ``params`` field, if it exists."""
47 |
48 | result: Any | None = attrs.field(converter=maybe_json)
49 | """The ``result`` field, if it exists."""
50 |
51 | error: Any | None = attrs.field(converter=maybe_json)
52 | """The ``error`` field, if it exists."""
53 |
54 | @classmethod
55 | def from_rpc(
56 | cls, session: str, timestamp: str, source: str, message: Mapping[str, Any]
57 | ):
58 | """Create an instance from a JSON-RPC message."""
59 | return cls(
60 | session=session,
61 | timestamp=datetime.fromisoformat(timestamp),
62 | source=source, # type: ignore
63 | id=message.get("id", None),
64 | method=message.get("method", None),
65 | params=message.get("params", None),
66 | result=message.get("result", None),
67 | error=message.get("error", None),
68 | )
69 |
70 | @property
71 | def is_request(self) -> bool:
72 | return self.id is not None and self.params is not None
73 |
74 | @property
75 | def is_response(self) -> bool:
76 | result_or_error = self.result is not None or self.error is not None
77 | return self.id is not None and result_or_error
78 |
79 | @property
80 | def is_notification(self) -> bool:
81 | return self.id is None and self.params is not None
82 |
83 |
84 | class LspHandler(logging.Handler):
85 | """Base class for lsp log handlers."""
86 |
87 | def __init__(self, *args, **kwargs):
88 | super().__init__(*args, **kwargs)
89 |
90 | def handle_message(self, message: LspMessage):
91 | """Called each time a message is processed."""
92 |
93 | def emit(self, record: logging.LogRecord):
94 | if not isinstance(record.args, dict):
95 | return
96 |
97 | message = record.args
98 | source = record.__dict__["Message-Source"]
99 |
100 | self.handle_message(
101 | LspMessage.from_rpc(
102 | session=record.__dict__["Message-Session"],
103 | timestamp=record.__dict__["Message-Timestamp"],
104 | source=source,
105 | message=message,
106 | )
107 | )
108 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/handlers/dbinit.sql:
--------------------------------------------------------------------------------
1 | -- Tables
2 |
3 | -- We use a single table 'protocol' to store all messages sent between client and server.
4 | -- Data within the table is then exposed through a number of SQL views, that parse out the
5 | -- details relevant to that view.
6 | CREATE TABLE IF NOT EXISTS protocol (
7 | session TEXT,
8 | timestamp REAL,
9 | source TEXT,
10 |
11 | id TEXT NULL,
12 | method TEXT NULL,
13 | params TEXT NULL,
14 | result TEXT NULL,
15 | error TEXT NULL
16 | );
17 |
18 | -- Views
19 |
20 | -- Requests
21 | CREATE VIEW IF NOT EXISTS requests AS
22 | SELECT
23 | client.session,
24 | client.timestamp,
25 | (server.timestamp - client.timestamp) * 1000 as duration,
26 | client.id,
27 | client.method,
28 | client.params,
29 | server.result,
30 | server.error
31 | FROM protocol as client
32 | INNER JOIN protocol as server ON
33 | client.session = server.session AND
34 | client.id = server.id AND
35 | client.params IS NOT NULL AND
36 | (
37 | server.result IS NOT NULL OR
38 | server.error IS NOT NULL
39 | );
40 |
41 | -- Notifications
42 | CREATE VIEW IF NOT EXISTS notifications AS
43 | SELECT
44 | rowid,
45 | session,
46 | timestamp,
47 | source,
48 | method,
49 | params
50 | FROM protocol
51 | WHERE id is NULL;
52 |
53 | -- Sessions
54 | CREATE VIEW IF NOT EXISTS sessions AS
55 | SELECT
56 | session,
57 | timestamp,
58 | json_extract(params, "$.clientInfo.name") as client_name,
59 | json_extract(params, "$.clientInfo.version") as client_version,
60 | json_extract(params, "$.rootUri") as root_uri,
61 | json_extract(params, "$.workspaceFolders") as workspace_folders,
62 | params,
63 | result
64 | FROM requests WHERE method = 'initialize';
65 |
66 | -- Log Messages
67 | CREATE VIEW IF NOT EXISTS logMessages AS
68 | SELECT
69 | rowid,
70 | session,
71 | timestamp,
72 | json_extract(params, "$.type") as type,
73 | json_extract(params, "$.message") as message
74 | FROM protocol
75 | WHERE method = 'window/logMessage';
76 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/handlers/sql.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pathlib
3 | import sqlite3
4 | from contextlib import closing
5 | from importlib import resources
6 |
7 | from lsp_devtools.handlers import LspHandler
8 | from lsp_devtools.handlers import LspMessage
9 |
10 |
11 | class SqlHandler(LspHandler):
12 | """A logging handler that sends log records to a SQL database"""
13 |
14 | def __init__(self, dbpath: pathlib.Path, *args, **kwargs):
15 | super().__init__(*args, **kwargs)
16 |
17 | self.dbpath = dbpath
18 |
19 | resource = resources.files("lsp_devtools.handlers").joinpath("dbinit.sql")
20 | sql_script = resource.read_text(encoding="utf8")
21 |
22 | with closing(sqlite3.connect(self.dbpath)) as conn:
23 | conn.executescript(sql_script)
24 |
25 | def handle_message(self, message: LspMessage):
26 | with closing(sqlite3.connect(self.dbpath)) as conn:
27 | cursor = conn.cursor()
28 | cursor.execute(
29 | "INSERT INTO protocol VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
30 | (
31 | message.session,
32 | message.timestamp,
33 | message.source,
34 | message.id,
35 | message.method,
36 | json.dumps(message.params) if message.params else None,
37 | json.dumps(message.result) if message.result else None,
38 | json.dumps(message.error) if message.error else None,
39 | ),
40 | )
41 |
42 | conn.commit()
43 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/inspector/app.css:
--------------------------------------------------------------------------------
1 | Sidebar {
2 | width: 60;
3 | dock: right;
4 | background: $panel;
5 | transition: offset 300ms out_cubic;
6 | }
7 | Sidebar.-hidden {
8 | offset-x: 100%;
9 | }
10 |
11 |
12 | DataTable {
13 | height: 100%;
14 | }
15 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/py.typed:
--------------------------------------------------------------------------------
1 | # Marker file for PEP 561.
2 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/lsp_devtools/record/filters.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | import typing
5 |
6 | import attrs
7 |
8 | from .formatters import FormatString
9 |
10 | if typing.TYPE_CHECKING:
11 | from typing import Literal
12 |
13 | MessageSource = Literal["client", "server", "both"]
14 | MessageType = Literal["request", "response", "result", "error", "notification"]
15 |
16 | logger = logging.getLogger(__name__)
17 |
18 |
19 | @attrs.define
20 | class LSPFilter(logging.Filter):
21 | """Logging filter for LSP messages."""
22 |
23 | message_source: MessageSource = attrs.field(default="both")
24 | """Only include messages from the given source."""
25 |
26 | include_message_types: set[MessageType] = attrs.field(factory=set, converter=set)
27 | """Only include the given message types."""
28 |
29 | exclude_message_types: set[MessageType] = attrs.field(factory=set, converter=set)
30 | """Exclude the given message types."""
31 |
32 | include_methods: set[str] = attrs.field(factory=set, converter=set)
33 | """Only include messages associated with the given method."""
34 |
35 | exclude_methods: set[str] = attrs.field(factory=set, converter=set)
36 | """Exclude messages associated with the given method."""
37 |
38 | formatter: FormatString = attrs.field(
39 | default="",
40 | converter=FormatString,
41 | ) # type: ignore
42 | """Format messages according to the given string"""
43 |
44 | _response_method_map: dict[int | str, str] = attrs.field(factory=dict)
45 | """Used to determine the method for response messages"""
46 |
47 | def filter(self, record: logging.LogRecord) -> bool:
48 | message = record.args
49 | if not isinstance(message, dict):
50 | return False
51 |
52 | source = record.__dict__["Message-Source"]
53 | message_type = get_message_type(message)
54 | message_method = self._get_message_method(message_type, message)
55 |
56 | if self.message_source not in {"both", source}:
57 | return False
58 |
59 | if self.include_message_types and not message_matches_type(
60 | message_type, self.include_message_types
61 | ):
62 | return False
63 |
64 | if self.exclude_message_types and message_matches_type(
65 | message_type, self.exclude_message_types
66 | ):
67 | return False
68 |
69 | if self.include_methods and message_method not in self.include_methods:
70 | return False
71 |
72 | if self.exclude_methods and message_method in self.exclude_methods:
73 | return False
74 |
75 | if self.formatter.pattern:
76 | try:
77 | record.msg = self.formatter.format(message)
78 | record.args = None
79 | except Exception:
80 | logger.debug(
81 | "Skipping message that failed to format: %s", message, exc_info=True
82 | )
83 | return False
84 |
85 | return True
86 |
87 | def _get_message_method(self, message_type: str, message: dict) -> str:
88 | if message_type == "request":
89 | method = message["method"]
90 | self._response_method_map[message["id"]] = method
91 |
92 | return method
93 |
94 | if message_type == "notification":
95 | return message["method"]
96 |
97 | return self._response_method_map[message["id"]]
98 |
99 |
100 | def message_matches_type(message_type: str, types: set[MessageType]) -> bool:
101 | """Determine if the type of message is included in the given set of types"""
102 |
103 | if message_type == "result":
104 | return len({"result", "response"} & types) > 0
105 |
106 | if message_type == "error":
107 | return len({"error", "response"} & types) > 0
108 |
109 | return message_type in types
110 |
111 |
112 | def get_message_type(message: dict) -> str:
113 | if "id" in message:
114 | if "error" in message:
115 | return "error"
116 | elif "method" in message:
117 | return "request"
118 | else:
119 | return "result"
120 | else:
121 | return "notification"
122 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/nix/lsp-devtools-overlay.nix:
--------------------------------------------------------------------------------
1 | final: prev:
2 |
3 | let
4 | # Read the package's version from file
5 | lines = prev.lib.splitString "\n" (builtins.readFile ../lsp_devtools/__init__.py);
6 | matches = builtins.map (builtins.match ''__version__ = "(.+)"'') lines;
7 | versionStr = prev.lib.concatStrings (prev.lib.flatten (builtins.filter builtins.isList matches));
8 | in {
9 | pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [(
10 | python-final: python-prev: {
11 |
12 | stamina = python-prev.buildPythonPackage rec {
13 | pname = "stamina";
14 | version = "24.1.0";
15 | format = "pyproject";
16 |
17 | src = prev.fetchFromGitHub {
18 | owner = "hynek";
19 | repo = "stamina";
20 | rev = "refs/tags/${version}";
21 | hash = "sha256-bIVzE9/QsdGw/UE83q3Q/XG3jFnPy65pkDdMpYkIrrs=";
22 | };
23 |
24 | SETUPTOOLS_SCM_PRETEND_VERSION = version;
25 | nativeBuildInputs = with python-final; [
26 | hatchling
27 | hatch-vcs
28 | hatch-fancy-pypi-readme
29 | ];
30 |
31 | propagatedBuildInputs = with python-final; [
32 | tenacity
33 | ] ++ prev.lib.optional (pythonOlder "3.10") typing-extensions;
34 |
35 | doCheck = true;
36 | pythonImportsCheck = [ "stamina" ];
37 | nativeCheckInputs = with python-prev; [
38 | anyio
39 | pytestCheckHook
40 | ];
41 |
42 | };
43 |
44 | lsp-devtools = python-prev.buildPythonPackage {
45 | pname = "lsp-devtools";
46 | version = versionStr;
47 | format = "pyproject";
48 |
49 | src = ./..;
50 |
51 | nativeBuildInputs = with python-final; [
52 | hatchling
53 | ];
54 |
55 | propagatedBuildInputs = with python-final; [
56 | aiosqlite
57 | platformdirs
58 | pygls
59 | stamina
60 | textual
61 | ] ++ prev.lib.optional (pythonOlder "3.9") importlib-resources
62 | ++ prev.lib.optional (pythonOlder "3.8") typing-extensions;
63 |
64 | doCheck = true;
65 | pythonImportsCheck = [ "lsp_devtools" ];
66 | nativeCheckInputs = with python-prev; [
67 | pytestCheckHook
68 | ];
69 |
70 | };
71 | }
72 | )];
73 | }
74 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling>=1.17.1"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "lsp-devtools"
7 | dynamic = ["version"]
8 | description = "Developer tooling for language servers"
9 | readme = "README.md"
10 | requires-python = ">=3.9"
11 | license = { text = "MIT" }
12 | authors = [{ name = "Alex Carney", email = "alcarneyme@gmail.com" }]
13 | classifiers = [
14 | "Development Status :: 3 - Alpha",
15 | "License :: OSI Approved :: MIT License",
16 | "Programming Language :: Python",
17 | "Programming Language :: Python :: 3",
18 | "Programming Language :: Python :: 3 :: Only",
19 | "Programming Language :: Python :: 3.9",
20 | "Programming Language :: Python :: 3.10",
21 | "Programming Language :: Python :: 3.11",
22 | "Programming Language :: Python :: 3.12",
23 | ]
24 | dependencies = [
25 | "aiosqlite",
26 | "platformdirs",
27 | "pygls>=2.0a2",
28 | "stamina",
29 | "textual>=0.41.0",
30 | ]
31 |
32 | [project.urls]
33 | "Bug Tracker" = "https://github.com/swyddfa/lsp-devtools/issues"
34 | "Documentation" = "https://lsp-devtools.readthedocs.io/en/latest/"
35 | "Source Code" = "https://github.com/swyddfa/lsp-devtools"
36 |
37 | [project.scripts]
38 | lsp-devtools = "lsp_devtools.cli:main"
39 |
40 | [tool.coverage.run]
41 | source_pkgs = ["lsp_devtools"]
42 |
43 | [tool.coverage.report]
44 | show_missing = true
45 | skip_covered = true
46 | sort = "Cover"
47 |
48 | [tool.pyright]
49 | include = ["lsp_devtools"]
50 | pythonVersion = "3.9"
51 |
52 | [tool.towncrier]
53 | filename = "CHANGES.md"
54 | directory = "changes/"
55 | title_format = "## v{version} - {project_date}"
56 | issue_format = "[#{issue}](https://github.com/swyddfa/lsp-devtools/issues/{issue})"
57 | underlines = ["", "", ""]
58 |
59 | type = [
60 | { name = "Features", directory = "feature", showcontent = true },
61 | { name = "Enhancements", directory = "enhancement", showcontent = true },
62 | { name = "Fixes", directory = "fix", showcontent = true },
63 | { name = "Docs", directory = "doc", showcontent = true },
64 | { name = "Breaking Changes", directory = "breaking", showcontent = true },
65 | { name = "Deprecated", directory = "deprecated", showcontent = true },
66 | { name = "Misc", directory = "misc", showcontent = true },
67 | ]
68 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/ruff.toml:
--------------------------------------------------------------------------------
1 | extend = "ruff_defaults.toml"
2 | line-length = 88
3 | indent-width = 4
4 |
5 | [format]
6 | # Be like black where possible
7 | quote-style = "double"
8 | indent-style = "space"
9 | line-ending = "auto"
10 | skip-magic-trailing-comma = false
11 |
12 | [lint]
13 | ignore = [
14 | "BLE001", # catch Exception:
15 | "INP001", # Complains about namespace packages
16 | "PT018", # Assertion should be broken down into multiple parts
17 | "T201", # print found
18 | "TRY003", # Exception message defined outside of class
19 |
20 | # The following were added when migrating to ruff, we might want to consider
21 | # enabling some of these again at some point.
22 | "A002", # argument shadowing
23 | "ARG001", # unused function argument
24 | "ARG002", # unused method argument
25 | "C405", # rewrite as set literal
26 | "C408", # dict(x=y)
27 | "C416", # Unecessary dict comprehension
28 | "C419", # Unecessary list comprehension
29 | "E402", # module import not at top of file
30 | "EM101", # raise ValueError("Literal string, not variable")
31 | "EM102", # raise ValueError(f"-string, not variable")
32 | "FBT001", # boolean arguments
33 | "FBT002", # boolean arguments
34 | "FLY002", # f-string alternative available
35 | "G003", # logging statement uses f-string
36 | "G004", # logging statement uses +
37 | "G201", # logging.error(.., exc_info=True)
38 | "N801", # naming conventions
39 | "N802", # naming conventions
40 | "N806", # naming conventions
41 | "PERF401", # use list comprehension
42 | "PERF402", # use list or list.copy
43 | "PLR2004", # magic values
44 | "PLW2901", # overwriting for-loop variable
45 | "PT006", # Complains about how `pytest.mark.parametrize` parameters are passed
46 | "PT011", # pytest.raises(ValueError)
47 | "RET503", # Missing return
48 | "RET504", # Unecessary assignment before return
49 | "RET505", # Unecessary elif after return
50 | "RUF001", # ambiguous characters
51 | "RUF012", # Mutable ClassVar annotation...
52 | "RUF015", # Prefer next(iter(...))
53 | "SIM102", # Use single if
54 | "SIM105", # Use contextlib.suppress(...)
55 | "SIM108", # Use ternary operator
56 | "SIM115", # Use key in dict
57 | "SIM118", # Use key in dict
58 | "SLF001", # private member access
59 | "TC001", # move import to type checking block
60 | "TC002", # move import to type checking block
61 | "TC003", # move import to type checking block
62 | "TID252", # Absolute vs relative imports
63 | "TRY300", # Move statement to else block
64 | ]
65 |
66 | [lint.per-file-ignores]
67 | "**/tests/**/*" = [
68 | "S",
69 | "SLF001", # private member accessed
70 | ]
71 |
72 | [lint.isort]
73 | force-single-line = true
74 |
75 | [lint.pyupgrade]
76 | # At least for now...
77 | keep-runtime-typing = true
78 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/tests/record/test_record.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import argparse
4 | import logging
5 | import typing
6 |
7 | import pytest
8 |
9 | from lsp_devtools.record import cli
10 | from lsp_devtools.record import setup_file_output
11 |
12 | if typing.TYPE_CHECKING:
13 | import pathlib
14 | from typing import Any
15 |
16 |
17 | @pytest.fixture(scope="module")
18 | def record():
19 | """Return a cli parser for the record command."""
20 | parser = argparse.ArgumentParser(description="for testing purposes")
21 | commands = parser.add_subparsers()
22 | cli(commands)
23 |
24 | return parser
25 |
26 |
27 | @pytest.fixture
28 | def logger():
29 | """Return the logger instance to use."""
30 |
31 | log = logging.getLogger(__name__)
32 | log.setLevel(logging.INFO)
33 |
34 | for handler in log.handlers:
35 | log.removeHandler(handler)
36 |
37 | return log
38 |
39 |
40 | @pytest.mark.parametrize(
41 | "args, messages, expected",
42 | [
43 | (
44 | [],
45 | [dict(jsonrpc="2.0", id=1, method="initialize", params=dict())],
46 | '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}\n',
47 | ),
48 | (
49 | ["-f", "{.|json-compact}"],
50 | [dict(jsonrpc="2.0", id=1, method="initialize", params=dict())],
51 | '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}\n',
52 | ),
53 | (
54 | ["-f", "{.|json}"],
55 | [dict(jsonrpc="2.0", id=1, method="initialize", params=dict())],
56 | "\n".join(
57 | [
58 | "{",
59 | ' "jsonrpc": "2.0",',
60 | ' "id": 1,',
61 | ' "method": "initialize",',
62 | ' "params": {}',
63 | "}",
64 | "",
65 | ]
66 | ),
67 | ),
68 | (
69 | ["-f", "{.method|json}"],
70 | [
71 | dict(jsonrpc="2.0", id=1, method="initialize", params=dict()),
72 | dict(jsonrpc="2.0", id=1, result=dict()),
73 | ],
74 | "initialize\n",
75 | ),
76 | (
77 | ["-f", "{.id}"],
78 | [
79 | dict(jsonrpc="2.0", id=1, method="initialize", params=dict()),
80 | dict(jsonrpc="2.0", id=1, result=dict()),
81 | ],
82 | "1\n1\n",
83 | ),
84 | ],
85 | )
86 | def test_file_output(
87 | tmp_path: pathlib.Path,
88 | record: argparse.ArgumentParser,
89 | logger: logging.Logger,
90 | args: list[str],
91 | messages: list[dict[str, Any]],
92 | expected: str,
93 | ):
94 | """Ensure that we can log to files correctly.
95 |
96 | Parameters
97 | ----------
98 | tmp_path
99 | pytest's ``tmp_path`` fixture
100 |
101 | record
102 | The record command's cli parser
103 |
104 | logger
105 | The logging instance to use
106 |
107 | messages
108 | The messages to record
109 |
110 | expected
111 | The expected file output.
112 | """
113 | log = tmp_path / "log.json"
114 | parsed_args = record.parse_args(["record", "--to-file", str(log), *args])
115 |
116 | setup_file_output(parsed_args, logger)
117 |
118 | for message in messages:
119 | logger.info("%s", message, extra={"Message-Source": "client"})
120 |
121 | assert log.read_text() == expected
122 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/tests/servers/simple.py:
--------------------------------------------------------------------------------
1 | """A very simple language server."""
2 |
3 | from lsprotocol import types
4 | from pygls.lsp.server import LanguageServer
5 |
6 | server = LanguageServer("simple-server", "v1")
7 |
8 |
9 | @server.feature(types.INITIALIZED)
10 | def _(ls: LanguageServer, params: types.InitializedParams):
11 | ls.window_show_message(
12 | types.ShowMessageParams(
13 | message="Hello, world!",
14 | type=types.MessageType.Log,
15 | )
16 | )
17 |
18 |
19 | if __name__ == "__main__":
20 | server.start_io()
21 |
--------------------------------------------------------------------------------
/lib/lsp-devtools/tests/test_agent.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import json
3 | import os
4 | import pathlib
5 | import subprocess
6 | import sys
7 |
8 | import pytest
9 |
10 | from lsp_devtools.agent import Agent
11 |
12 | SERVER_DIR = pathlib.Path(__file__).parent / "servers"
13 |
14 |
15 | def format_message(obj):
16 | content = json.dumps(obj)
17 | message = "".join(
18 | [
19 | f"Content-Length: {len(content)}\r\n",
20 | "\r\n",
21 | f"{content}",
22 | ]
23 | )
24 | return message.encode()
25 |
26 |
27 | def echo_handler(d: bytes):
28 | sys.stdout.buffer.write(d)
29 | sys.stdout.flush()
30 |
31 |
32 | @pytest.mark.asyncio
33 | async def test_agent_exits():
34 | """Ensure that when the client closes down the lsp session and the server process
35 | exits, the agent does also."""
36 |
37 | (stdin_read, stdin_write) = os.pipe()
38 | (stdout_read, stdout_write) = os.pipe()
39 |
40 | server = await asyncio.create_subprocess_exec(
41 | sys.executable,
42 | str(SERVER_DIR / "simple.py"),
43 | stdin=subprocess.PIPE,
44 | stdout=subprocess.PIPE,
45 | stderr=subprocess.PIPE,
46 | )
47 |
48 | agent = Agent(
49 | server,
50 | os.fdopen(stdin_read, mode="rb"),
51 | os.fdopen(stdout_write, mode="wb"),
52 | echo_handler,
53 | )
54 |
55 | os.write(
56 | stdin_write,
57 | format_message(
58 | dict(jsonrpc="2.0", id=1, method="initialize", params=dict(capabilities={}))
59 | ),
60 | )
61 |
62 | os.write(
63 | stdin_write,
64 | format_message(dict(jsonrpc="2.0", id=2, method="shutdown", params=None)),
65 | )
66 |
67 | os.write(
68 | stdin_write,
69 | format_message(dict(jsonrpc="2.0", method="exit", params=None)),
70 | )
71 |
72 | try:
73 | await asyncio.wait_for(
74 | # asyncio.gather(server.wait(), agent.start()),
75 | agent.start(),
76 | timeout=10, # s
77 | )
78 | except asyncio.CancelledError:
79 | pass # The agent's tasks should be cancelled
80 |
81 | except TimeoutError as exc:
82 | # Make sure this timed out for the right reason.
83 | if server.returncode is None:
84 | raise RuntimeError("Server process did not exit") from exc
85 |
86 | exc.add_note("lsp-devtools agent did not stop")
87 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 0.3.1
3 | commit = False
4 | tag = False
5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(.dev(?P\d+))?
6 | serialize =
7 | {major}.{minor}.{patch}.dev{dev}
8 | {major}.{minor}.{patch}
9 |
10 | [bumpversion:file:pytest_lsp/client.py]
11 |
12 | [bumpversion:file:pyproject.toml]
13 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/.gitignore:
--------------------------------------------------------------------------------
1 | .coverage
2 | result
3 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Alex Carney
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include pytest_lsp/clients/*.json
2 | include pytest_lsp/py.typed
3 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/Makefile:
--------------------------------------------------------------------------------
1 | PY ?= 310
2 | COMMAND ?=
3 |
4 | ifdef COMMAND
5 | CMD = --command $(COMMAND)
6 | endif
7 |
8 | .PHONY: develop test repl
9 |
10 |
11 | repl: CMD = --command python
12 | repl: develop
13 |
14 |
15 | test: CMD = --command pytest $(ARGS)
16 | test: develop
17 |
18 |
19 | develop:
20 | nix develop .#py$(PY) $(CMD)
21 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/README.md:
--------------------------------------------------------------------------------
1 | # pytest-lsp: End-to-end testing of language servers with pytest
2 |
3 | `pytest-lsp` is a pytest plugin for writing end-to-end tests for language servers.
4 |
5 | It works by running the language server in a subprocess and communicating with it over stdio, just like a real language client.
6 | This also means `pytest-lsp` can be used to test language servers written in any language - not just Python.
7 |
8 | `pytest-lsp` relies on the [`pygls`](https://github.com/openlawlibrary/pygls) library for its language server protocol implementation.
9 |
10 | See the [documentation](https://lsp-devtools.readthedocs.io/en/latest/) for details on getting started.
11 |
12 | ```python
13 | import sys
14 |
15 | import pytest
16 | import pytest_lsp
17 | from lsprotocol import types
18 | from pytest_lsp import (
19 | ClientServerConfig,
20 | LanguageClient,
21 | client_capabilities,
22 | )
23 |
24 |
25 | @pytest_lsp.fixture(
26 | scope="module",
27 | config=ClientServerConfig(
28 | server_command=[sys.executable, "-m", "esbonio"],
29 | ),
30 | )
31 | async def client(lsp_client: LanguageClient):
32 | # Setup
33 | response = await lsp_client.initialize_session(
34 | types.InitializeParams(
35 | capabilities=client_capabilities("visual-studio-code"),
36 | workspace_folders=[
37 | types.WorkspaceFolder(
38 | uri="file:///path/to/test/project/root/", name="project"
39 | ),
40 | ],
41 | )
42 | )
43 |
44 | yield
45 |
46 | # Teardown
47 | await lsp_client.shutdown_session()
48 |
49 |
50 | @pytest.mark.asyncio(loop_scope="module")
51 | async def test_completion(client: LanguageClient):
52 | result = await client.text_document_completion_async(
53 | params=types.CompletionParams(
54 | position=types.Position(line=5, character=23),
55 | text_document=types.TextDocumentIdentifier(
56 | uri="file:///path/to/test/project/root/test_file.rst"
57 | ),
58 | )
59 | )
60 |
61 | assert len(result.items) > 0
62 | ```
63 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/changes/github-template.html:
--------------------------------------------------------------------------------
1 | %(body_pre_docinfo)s
2 | %(docinfo)s
3 | %(body)s
4 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "nixpkgs": {
4 | "locked": {
5 | "lastModified": 1704161960,
6 | "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=",
7 | "owner": "NixOS",
8 | "repo": "nixpkgs",
9 | "rev": "63143ac2c9186be6d9da6035fa22620018c85932",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "NixOS",
14 | "ref": "nixpkgs-unstable",
15 | "repo": "nixpkgs",
16 | "type": "github"
17 | }
18 | },
19 | "root": {
20 | "inputs": {
21 | "nixpkgs": "nixpkgs",
22 | "utils": "utils"
23 | }
24 | },
25 | "systems": {
26 | "locked": {
27 | "lastModified": 1681028828,
28 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
29 | "owner": "nix-systems",
30 | "repo": "default",
31 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
32 | "type": "github"
33 | },
34 | "original": {
35 | "owner": "nix-systems",
36 | "repo": "default",
37 | "type": "github"
38 | }
39 | },
40 | "utils": {
41 | "inputs": {
42 | "systems": "systems"
43 | },
44 | "locked": {
45 | "lastModified": 1701680307,
46 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
47 | "owner": "numtide",
48 | "repo": "flake-utils",
49 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "numtide",
54 | "repo": "flake-utils",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "pytest-lsp: End to end testing of language servers with pytest";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6 | utils.url = "github:numtide/flake-utils";
7 | };
8 |
9 | outputs = { self, nixpkgs, utils }:
10 |
11 | let
12 | eachPythonVersion = versions: f:
13 | builtins.listToAttrs (builtins.map (version: {name = "py${version}"; value = f version; }) versions);
14 | pytest-lsp-overlay = import ./nix/pytest-lsp-overlay.nix;
15 | in {
16 |
17 | overlays.default = pytest-lsp-overlay;
18 |
19 | packages = utils.lib.eachDefaultSystemMap (system:
20 | let
21 | pkgs = import nixpkgs { inherit system; overlays = [ pytest-lsp-overlay ]; };
22 | in
23 | eachPythonVersion [ "38" "39" "310" "311"] (pyVersion:
24 | pkgs."python${pyVersion}Packages".pytest-lsp
25 | )
26 | );
27 |
28 | devShells = utils.lib.eachDefaultSystemMap (system:
29 | let
30 | pkgs = import nixpkgs { inherit system; overlays = [ pytest-lsp-overlay ]; };
31 | in
32 | eachPythonVersion [ "38" "39" "310" "311" ] (pyVersion:
33 | with pkgs; mkShell {
34 | name = "py${pyVersion}";
35 |
36 | shellHook = ''
37 | export PYTHONPATH="./:$PYTHONPATH"
38 | '';
39 |
40 | packages = with pkgs."python${pyVersion}Packages"; [
41 | pygls
42 | pytest
43 | pytest-asyncio
44 | ];
45 | }
46 | )
47 | );
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/hatch.toml:
--------------------------------------------------------------------------------
1 | [version]
2 | path = "pytest_lsp/client.py"
3 | validate-bump = false
4 |
5 | [build.targets.sdist]
6 | include = ["pytest_lsp", "tests", "CHANGES.md"]
7 |
8 | [build.targets.wheel]
9 | packages = ["pytest_lsp"]
10 |
11 | [envs.hatch-test]
12 | dependencies = ["pytest-asyncio"]
13 |
14 | [envs.hatch-test.env-vars]
15 | UV_PRERELEASE="allow"
16 |
17 | [[envs.hatch-test.matrix]]
18 | python = ["3.9", "3.10", "3.11", "3.12", "3.13"]
19 | pytest = ["8"]
20 |
21 | [envs.hatch-test.overrides]
22 | matrix.pytest.dependencies = [
23 | { value = "pytest>=8,<9", if = ["8"] },
24 | ]
25 |
26 | [envs.hatch-static-analysis]
27 | config-path = "ruff_defaults.toml"
28 | dependencies = ["ruff==0.8.0"]
29 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/nix/pytest-lsp-overlay.nix:
--------------------------------------------------------------------------------
1 | final: prev:
2 |
3 | let
4 | # Read the package's version from file
5 | lines = prev.lib.splitString "\n" (builtins.readFile ../pytest_lsp/client.py);
6 | matches = builtins.map (builtins.match ''__version__ = "(.+)"'') lines;
7 | versionStr = prev.lib.concatStrings (prev.lib.flatten (builtins.filter builtins.isList matches));
8 | in {
9 | pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [(
10 | python-final: python-prev: {
11 |
12 | pytest-lsp = python-prev.buildPythonPackage {
13 | pname = "pytest-lsp";
14 | version = versionStr;
15 | format = "pyproject";
16 |
17 | src = ./..;
18 |
19 | nativeBuildInputs = with python-final; [
20 | hatchling
21 | ];
22 |
23 | propagatedBuildInputs = with python-final; [
24 | pygls
25 | pytest
26 | pytest-asyncio
27 | ];
28 |
29 | doCheck = true;
30 | pythonImportsCheck = [ "pytest_lsp" ];
31 | nativeCheckInputs = with python-prev; [
32 | pytestCheckHook
33 | ];
34 |
35 | };
36 | }
37 | )];
38 | }
39 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling>=1.17.1"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "pytest-lsp"
7 | dynamic = ["version"]
8 | description = "A pytest plugin for end-to-end testing of language servers"
9 | readme = "README.md"
10 | requires-python = ">=3.9"
11 | license = { text = "MIT" }
12 | authors = [{ name = "Alex Carney", email = "alcarneyme@gmail.com" }]
13 | classifiers = [
14 | "Development Status :: 4 - Beta",
15 | "License :: OSI Approved :: MIT License",
16 | "Framework :: Pytest",
17 | "Programming Language :: Python",
18 | "Programming Language :: Python :: 3",
19 | "Programming Language :: Python :: 3 :: Only",
20 | "Programming Language :: Python :: 3.9",
21 | "Programming Language :: Python :: 3.10",
22 | "Programming Language :: Python :: 3.11",
23 | "Programming Language :: Python :: 3.12",
24 | "Programming Language :: Python :: 3.13",
25 | ]
26 | dependencies = [
27 | "packaging",
28 | "pygls>=2.0.0a3",
29 | "pytest",
30 | "pytest-asyncio>=0.24",
31 | ]
32 |
33 | [project.urls]
34 | "Bug Tracker" = "https://github.com/swyddfa/lsp-devtools/issues"
35 | "Documentation" = "https://lsp-devtools.readthedocs.io/en/latest/"
36 | "Source Code" = "https://github.com/swyddfa/lsp-devtools"
37 |
38 | [project.entry-points.pytest11]
39 | pytest-lsp = "pytest_lsp"
40 |
41 | [tool.coverage.run]
42 | source_pkgs = ["pytest_lsp"]
43 |
44 | [tool.coverage.report]
45 | show_missing = true
46 | skip_covered = true
47 | sort = "Cover"
48 |
49 | [tool.pytest.ini_options]
50 | asyncio_mode = "auto"
51 | asyncio_default_fixture_loop_scope = "function"
52 |
53 | [tool.towncrier]
54 | filename = "CHANGES.md"
55 | directory = "changes/"
56 | title_format = "## v{version} - {project_date}"
57 | issue_format = "[#{issue}](https://github.com/swyddfa/lsp-devtools/issues/{issue})"
58 | underlines = ["", "", ""]
59 |
60 | type = [
61 | { name = "Features", directory = "feature", showcontent = true },
62 | { name = "Enhancements", directory = "enhancement", showcontent = true },
63 | { name = "Client Capabilities", directory = "capability", showcontent = true },
64 | { name = "Fixes", directory = "fix", showcontent = true },
65 | { name = "Docs", directory = "doc", showcontent = true },
66 | { name = "Breaking Changes", directory = "breaking", showcontent = true },
67 | { name = "Deprecated", directory = "deprecated", showcontent = true },
68 | { name = "Misc", directory = "misc", showcontent = true },
69 | { name = "Removed", directory = "removed", showcontent = true },
70 | ]
71 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/pytest_lsp/__init__.py:
--------------------------------------------------------------------------------
1 | from .checks import LspSpecificationWarning
2 | from .client import LanguageClient
3 | from .client import __version__
4 | from .client import client_capabilities
5 | from .client import make_test_lsp_client
6 | from .plugin import ClientServerConfig
7 | from .plugin import fixture
8 | from .plugin import pytest_addoption
9 | from .plugin import pytest_runtest_makereport
10 | from .protocol import LanguageClientProtocol
11 |
12 | __all__ = [
13 | "__version__",
14 | "ClientServerConfig",
15 | "LanguageClient",
16 | "LanguageClientProtocol",
17 | "LspSpecificationWarning",
18 | "client_capabilities",
19 | "fixture",
20 | "make_test_lsp_client",
21 | "pytest_addoption",
22 | "pytest_runtest_makereport",
23 | ]
24 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/pytest_lsp/clients/README.md:
--------------------------------------------------------------------------------
1 | # Clients
2 |
3 | This folder contains captured [`ClientCapabilities`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#clientCapabilities) responses from various language clients at various versions.
4 |
5 | These snapshots are how `pytest-lsp` plugin is able to impersonate different language clients when testing a server.
6 | They also power the [Client Capability Index](https://lsp-devtools.readthedocs.io/en/latest/#client-capability-index) section of the documentation.
7 |
8 | Each filename follows the `_v.json` naming convention and contain the following fields of an [`InitializeParams`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeParams) object.
9 |
10 | ```json
11 | {
12 | "clientInfo": {
13 | "name": "Client Name",
14 | "version": "1.2.3"
15 | },
16 | "capabilities": { ... }
17 | }
18 | ```
19 |
20 | ## Adding new clients and versions
21 |
22 | ### Neovim
23 |
24 | Adding new neovim versions has been semi-automated through a [Github Actions workflow](https://github.com/swyddfa/lsp-devtools/blob/develop/.github/workflows/capabilities-nvim.yml)
25 |
26 | ### Other Clients
27 |
28 | This can be done with `lsp-devtools` command.
29 |
30 | 1. Configure the language client to invoke a language server [wrapped with the lsp-devtools agent](https://lsp-devtools.readthedocs.io/en/latest/lsp-devtools/guide/getting-started.html#configuring-your-client).
31 |
32 | 1. In a separate terminal, use the following `lsp-devtools` command to record the necessary information to a JSON file
33 | ```
34 | lsp-devtools record -f '{{"clientInfo": {.params.clientInfo}, "capabilities": {.params.capabilities}}}' --to-file _v.json
35 | ```
36 |
37 | 1. Run the language server via the client to generate the necessary data, once the server is ready to use you should be able to close the client.
38 |
39 | 1. The `lsp-devtools record` command should have exited with a file called `_v.json` saved to your working directory.
40 |
41 | 1. Open a pull request adding the file to this folder!
42 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/pytest_lsp/clients/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swyddfa/lsp-devtools/bc9c2973dbc648f6026122172b016917827fa97a/lib/pytest-lsp/pytest_lsp/clients/__init__.py
--------------------------------------------------------------------------------
/lib/pytest-lsp/pytest_lsp/clients/emacs_v29.1.json:
--------------------------------------------------------------------------------
1 | {
2 | "clientInfo": {
3 | "name": "Emacs (eglot)",
4 | "version": "29.1"
5 | },
6 | "capabilities": {
7 | "workspace": {
8 | "applyEdit": true,
9 | "executeCommand": {
10 | "dynamicRegistration": false
11 | },
12 | "workspaceEdit": {
13 | "documentChanges": true
14 | },
15 | "didChangeWatchedFiles": {
16 | "dynamicRegistration": true
17 | },
18 | "symbol": {
19 | "dynamicRegistration": false
20 | },
21 | "configuration": true,
22 | "workspaceFolders": true
23 | },
24 | "textDocument": {
25 | "synchronization": {
26 | "dynamicRegistration": false,
27 | "willSave": true,
28 | "willSaveWaitUntil": true,
29 | "didSave": true
30 | },
31 | "completion": {
32 | "dynamicRegistration": false,
33 | "completionItem": {
34 | "snippetSupport": false,
35 | "deprecatedSupport": true,
36 | "resolveSupport": {
37 | "properties": [
38 | "documentation",
39 | "details",
40 | "additionalTextEdits"
41 | ]
42 | },
43 | "tagSupport": {
44 | "valueSet": [
45 | 1
46 | ]
47 | }
48 | },
49 | "contextSupport": true
50 | },
51 | "hover": {
52 | "dynamicRegistration": false,
53 | "contentFormat": [
54 | "markdown",
55 | "plaintext"
56 | ]
57 | },
58 | "signatureHelp": {
59 | "dynamicRegistration": false,
60 | "signatureInformation": {
61 | "parameterInformation": {
62 | "labelOffsetSupport": true
63 | },
64 | "activeParameterSupport": true
65 | }
66 | },
67 | "references": {
68 | "dynamicRegistration": false
69 | },
70 | "definition": {
71 | "dynamicRegistration": false,
72 | "linkSupport": true
73 | },
74 | "declaration": {
75 | "dynamicRegistration": false,
76 | "linkSupport": true
77 | },
78 | "implementation": {
79 | "dynamicRegistration": false,
80 | "linkSupport": true
81 | },
82 | "typeDefinition": {
83 | "dynamicRegistration": false,
84 | "linkSupport": true
85 | },
86 | "documentSymbol": {
87 | "dynamicRegistration": false,
88 | "hierarchicalDocumentSymbolSupport": true,
89 | "symbolKind": {
90 | "valueSet": [
91 | 1,
92 | 2,
93 | 3,
94 | 4,
95 | 5,
96 | 6,
97 | 7,
98 | 8,
99 | 9,
100 | 10,
101 | 11,
102 | 12,
103 | 13,
104 | 14,
105 | 15,
106 | 16,
107 | 17,
108 | 18,
109 | 19,
110 | 20,
111 | 21,
112 | 22,
113 | 23,
114 | 24,
115 | 25,
116 | 26
117 | ]
118 | }
119 | },
120 | "documentHighlight": {
121 | "dynamicRegistration": false
122 | },
123 | "codeAction": {
124 | "dynamicRegistration": false,
125 | "codeActionLiteralSupport": {
126 | "codeActionKind": {
127 | "valueSet": [
128 | "quickfix",
129 | "refactor",
130 | "refactor.extract",
131 | "refactor.inline",
132 | "refactor.rewrite",
133 | "source",
134 | "source.organizeImports"
135 | ]
136 | }
137 | },
138 | "isPreferredSupport": true
139 | },
140 | "formatting": {
141 | "dynamicRegistration": false
142 | },
143 | "rangeFormatting": {
144 | "dynamicRegistration": false
145 | },
146 | "rename": {
147 | "dynamicRegistration": false
148 | },
149 | "inlayHint": {
150 | "dynamicRegistration": false
151 | },
152 | "publishDiagnostics": {
153 | "relatedInformation": false,
154 | "codeDescriptionSupport": false,
155 | "tagSupport": {
156 | "valueSet": [
157 | 1,
158 | 2
159 | ]
160 | }
161 | }
162 | },
163 | "window": {
164 | "workDoneProgress": true
165 | },
166 | "general": {
167 | "positionEncodings": [
168 | "utf-32",
169 | "utf-8",
170 | "utf-16"
171 | ]
172 | },
173 | "experimental": {}
174 | }
175 | }
--------------------------------------------------------------------------------
/lib/pytest-lsp/pytest_lsp/protocol.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 | import logging
5 | import typing
6 | from concurrent.futures import Future
7 |
8 | from pygls.protocol import LanguageServerProtocol
9 |
10 | from .checks import check_params_against_client_capabilities
11 | from .checks import check_result_against_client_capabilities
12 |
13 | if typing.TYPE_CHECKING:
14 | from .client import LanguageClient
15 |
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | class LanguageClientProtocol(LanguageServerProtocol):
21 | """An extended protocol class adding functionality useful for testing."""
22 |
23 | _server: LanguageClient # type: ignore[assignment]
24 |
25 | def __init__(self, *args, **kwargs):
26 | super().__init__(*args, **kwargs)
27 |
28 | self._notification_futures = {}
29 |
30 | def _handle_request(self, msg_id, method_name, params):
31 | """Wrap pygls' handle_request implementation. This will
32 |
33 | - Check if the request from the server is compatible with the client's stated
34 | capabilities.
35 |
36 | """
37 | check_params_against_client_capabilities(
38 | self._server.capabilities, method_name, params
39 | )
40 | return super()._handle_request(msg_id, method_name, params)
41 |
42 | def _handle_notification(self, method_name, params):
43 | """Wrap pygls' handle_notification implementation. This will
44 |
45 | - Notify a future waiting on the notification, if applicable.
46 |
47 | - Check the params to see if they are compatible with the client's stated
48 | capabilities.
49 |
50 | """
51 | future = self._notification_futures.pop(method_name, None)
52 | if future:
53 | future.set_result(params)
54 |
55 | super()._handle_notification(method_name, params)
56 |
57 | async def send_request_async(self, method, params=None, msg_id=None):
58 | """Wrap pygls' ``send_request_async`` implementation. This will
59 |
60 | - Check the params to see if they're compatible with the client's stated
61 | capabilities
62 | - Check the result to see if it's compatible with the client's stated
63 | capabilities
64 |
65 | Parameters
66 | ----------
67 | method
68 | The method name of the request to send
69 |
70 | params
71 | The associated parameters to go with the request
72 |
73 | Returns
74 | -------
75 | Any
76 | The result
77 | """
78 | check_params_against_client_capabilities(
79 | self._server.capabilities, method, params
80 | )
81 | result = await super().send_request_async(method, params, msg_id)
82 | check_result_against_client_capabilities(
83 | self._server.capabilities,
84 | method,
85 | result, # type: ignore
86 | )
87 |
88 | return result
89 |
90 | def wait_for_notification(self, method: str, callback=None) -> Future:
91 | """Wait for a notification message with the given ``method``.
92 |
93 | Parameters
94 | ----------
95 | method
96 | The method name to wait for
97 |
98 | callback
99 | If given, ``callback`` will be called with the notification message's
100 | ``params`` when recevied
101 |
102 | Returns
103 | -------
104 | Future
105 | A future that will resolve when the requested notification message is
106 | recevied.
107 | """
108 | future: Future = Future()
109 | if callback:
110 |
111 | def wrapper(future: Future):
112 | result = future.result()
113 | callback(result)
114 |
115 | future.add_done_callback(wrapper)
116 |
117 | self._notification_futures[method] = future
118 | return future
119 |
120 | def wait_for_notification_async(self, method: str):
121 | """Wait for a notification message with the given ``method``.
122 |
123 | Parameters
124 | ----------
125 | method
126 | The method name to wait for
127 |
128 | Returns
129 | -------
130 | Any
131 | The notification message's ``params``
132 | """
133 | future = self.wait_for_notification(method)
134 | return asyncio.wrap_future(future)
135 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/pytest_lsp/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swyddfa/lsp-devtools/bc9c2973dbc648f6026122172b016917827fa97a/lib/pytest-lsp/pytest_lsp/py.typed
--------------------------------------------------------------------------------
/lib/pytest-lsp/ruff.toml:
--------------------------------------------------------------------------------
1 | extend = "ruff_defaults.toml"
2 | line-length = 88
3 | indent-width = 4
4 |
5 | [format]
6 | # Be like black where possible
7 | quote-style = "double"
8 | indent-style = "space"
9 | line-ending = "auto"
10 | skip-magic-trailing-comma = false
11 |
12 | [lint]
13 | ignore = [
14 | "BLE001", # catch Exception:
15 | "INP001", # Complains about namespace packages
16 | "PT018", # Assertion should be broken down into multiple parts
17 | "T201", # print found
18 | "TRY003", # Exception message defined outside of class
19 |
20 | # The following were added when migrating to ruff, we might want to consider
21 | # enabling some of these again at some point.
22 | "A002", # argument shadowing
23 | "ARG001", # unused function argument
24 | "ARG002", # unused method argument
25 | "C405", # rewrite as set literal
26 | "C408", # dict(x=y)
27 | "C416", # Unecessary dict comprehension
28 | "C419", # Unecessary list comprehension
29 | "E402", # module import not at top of file
30 | "EM101", # raise ValueError("Literal string, not variable")
31 | "EM102", # raise ValueError(f"-string, not variable")
32 | "FBT001", # boolean arguments
33 | "FBT002", # boolean arguments
34 | "FLY002", # f-string alternative available
35 | "G003", # logging statement uses f-string
36 | "G004", # logging statement uses +
37 | "G201", # logging.error(.., exc_info=True)
38 | "N801", # naming conventions
39 | "N802", # naming conventions
40 | "N806", # naming conventions
41 | "PERF401", # use list comprehension
42 | "PERF402", # use list or list.copy
43 | "PLR2004", # magic values
44 | "PLW2901", # overwriting for-loop variable
45 | "PT006", # Complains about how `pytest.mark.parametrize` parameters are passed
46 | "PT011", # pytest.raises(ValueError)
47 | "RET503", # Missing return
48 | "RET504", # Unecessary assignment before return
49 | "RET505", # Unecessary elif after return
50 | "RUF001", # ambiguous characters
51 | "RUF012", # Mutable ClassVar annotation...
52 | "RUF015", # Prefer next(iter(...))
53 | "SIM102", # Use single if
54 | "SIM105", # Use contextlib.suppress(...)
55 | "SIM108", # Use ternary operator
56 | "SIM115", # Use key in dict
57 | "SIM118", # Use key in dict
58 | "SLF001", # private member access
59 | "TC001", # move import to type checking block
60 | "TC002", # move import to type checking block
61 | "TC003", # move import to type checking block
62 | "TID252", # Absolute vs relative imports
63 | "TRY300", # Move statement to else block
64 | ]
65 |
66 | [lint.per-file-ignores]
67 | "**/tests/**/*" = [
68 | "S",
69 | "SLF001", # private member accessed
70 | ]
71 |
72 | [lint.isort]
73 | force-single-line = true
74 |
75 | [lint.pyupgrade]
76 | # At least for now...
77 | keep-runtime-typing = true
78 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/conftest.py:
--------------------------------------------------------------------------------
1 | pytest_plugins = ["pytester"]
2 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/client-capabilities/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol.types import (
2 | TEXT_DOCUMENT_COMPLETION,
3 | CompletionItem,
4 | CompletionParams,
5 | InsertTextFormat,
6 | )
7 | from pygls.lsp.server import LanguageServer
8 |
9 | server = LanguageServer("hello-world", "v1")
10 |
11 |
12 | @server.feature(TEXT_DOCUMENT_COMPLETION)
13 | def completion(ls: LanguageServer, params: CompletionParams):
14 | return [
15 | CompletionItem(
16 | label="greet",
17 | insert_text='"Hello, ${1:name}!"$0',
18 | insert_text_format=InsertTextFormat.Snippet,
19 | ),
20 | ]
21 |
22 |
23 | if __name__ == "__main__":
24 | server.start_io()
25 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/client-capabilities/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest_lsp
4 | from lsprotocol.types import (
5 | CompletionList,
6 | CompletionParams,
7 | InitializeParams,
8 | Position,
9 | TextDocumentIdentifier,
10 | )
11 | from pytest_lsp import ClientServerConfig, LanguageClient, client_capabilities
12 |
13 |
14 | @pytest_lsp.fixture(
15 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
16 | )
17 | async def client(lsp_client: LanguageClient):
18 | # Setup
19 | await lsp_client.initialize_session(
20 | InitializeParams(
21 | capabilities=client_capabilities("neovim"),
22 | ),
23 | )
24 |
25 | yield
26 |
27 | # Teardown
28 | await lsp_client.shutdown_session()
29 |
30 |
31 | async def test_completions(client: LanguageClient):
32 | """Ensure that the server implements completions correctly."""
33 |
34 | results = await client.text_document_completion_async(
35 | params=CompletionParams(
36 | position=Position(line=1, character=0),
37 | text_document=TextDocumentIdentifier(uri="file:///path/to/file.txt"),
38 | )
39 | )
40 | assert results is not None
41 |
42 | if isinstance(results, CompletionList):
43 | items = results.items
44 | else:
45 | items = results
46 |
47 | labels = [item.label for item in items]
48 | assert labels == ["greet"]
49 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/diagnostics/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol import types
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("diagnostic-server", "v1")
5 |
6 |
7 | @server.feature(types.TEXT_DOCUMENT_DID_OPEN)
8 | def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams):
9 | ls.text_document_publish_diagnostics(
10 | types.PublishDiagnosticsParams(
11 | uri=params.text_document.uri,
12 | diagnostics=[
13 | types.Diagnostic(
14 | message="There is an error here.",
15 | range=types.Range(
16 | start=types.Position(line=1, character=1),
17 | end=types.Position(line=1, character=10),
18 | ),
19 | )
20 | ],
21 | )
22 | )
23 |
24 |
25 | if __name__ == "__main__":
26 | server.start_io()
27 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/diagnostics/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol.types import (
6 | TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS,
7 | ClientCapabilities,
8 | DidOpenTextDocumentParams,
9 | InitializeParams,
10 | TextDocumentItem,
11 | )
12 | from pytest_lsp import ClientServerConfig, LanguageClient
13 |
14 |
15 | @pytest_lsp.fixture(
16 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
17 | )
18 | async def client(lsp_client: LanguageClient):
19 | # Setup
20 | params = InitializeParams(capabilities=ClientCapabilities())
21 | await lsp_client.initialize_session(params)
22 |
23 | yield
24 |
25 | # Teardown
26 | await lsp_client.shutdown_session()
27 |
28 |
29 | @pytest.mark.asyncio
30 | async def test_diagnostics(client: LanguageClient):
31 | """Ensure that the server implements diagnostics correctly."""
32 |
33 | test_uri = "file:///path/to/file.txt"
34 | client.text_document_did_open(
35 | DidOpenTextDocumentParams(
36 | text_document=TextDocumentItem(
37 | uri=test_uri,
38 | language_id="plaintext",
39 | version=1,
40 | text="The file's contents",
41 | )
42 | )
43 | )
44 |
45 | # Wait for the server to publish its diagnostics
46 | await client.wait_for_notification(TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS)
47 |
48 | assert test_uri in client.diagnostics
49 | assert client.diagnostics[test_uri][0].message == "There is an error here."
50 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/fixture-passthrough/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol.types import TEXT_DOCUMENT_COMPLETION, CompletionItem, CompletionParams
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("hello-world", "v1")
5 |
6 |
7 | @server.feature(TEXT_DOCUMENT_COMPLETION)
8 | def completion(ls: LanguageServer, params: CompletionParams):
9 | return [
10 | CompletionItem(label="hello"),
11 | CompletionItem(label="world"),
12 | ]
13 |
14 |
15 | if __name__ == "__main__":
16 | server.start_io()
17 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/fixture-passthrough/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol.types import (
6 | CompletionList,
7 | CompletionParams,
8 | InitializeParams,
9 | Position,
10 | TextDocumentIdentifier,
11 | )
12 | from pytest_lsp import ClientServerConfig, LanguageClient, client_capabilities
13 |
14 |
15 | @pytest.fixture(scope="module")
16 | def client_name():
17 | return "neovim"
18 |
19 |
20 | @pytest_lsp.fixture(
21 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
22 | )
23 | async def client(client_name: str, lsp_client: LanguageClient):
24 | # Setup
25 | params = InitializeParams(capabilities=client_capabilities(client_name))
26 | await lsp_client.initialize_session(params)
27 |
28 | yield
29 |
30 | # Teardown
31 | await lsp_client.shutdown_session()
32 |
33 |
34 | async def test_completions(client: LanguageClient):
35 | """Ensure that the server implements completions correctly."""
36 |
37 | results = await client.text_document_completion_async(
38 | params=CompletionParams(
39 | position=Position(line=1, character=0),
40 | text_document=TextDocumentIdentifier(uri="file:///path/to/file.txt"),
41 | )
42 | )
43 | assert results is not None
44 |
45 | if isinstance(results, CompletionList):
46 | items = results.items
47 | else:
48 | items = results
49 |
50 | labels = [item.label for item in items]
51 | assert labels == ["hello", "world"]
52 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/fixture-scope/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol.types import TEXT_DOCUMENT_COMPLETION, CompletionItem, CompletionParams
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("hello-world", "v1")
5 |
6 |
7 | @server.feature(TEXT_DOCUMENT_COMPLETION)
8 | def completion(ls: LanguageServer, params: CompletionParams):
9 | return [
10 | CompletionItem(label="hello"),
11 | CompletionItem(label="world"),
12 | ]
13 |
14 |
15 | if __name__ == "__main__":
16 | server.start_io()
17 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/fixture-scope/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol import types
6 | from pytest_lsp import ClientServerConfig, LanguageClient
7 |
8 |
9 | @pytest_lsp.fixture(
10 | scope="module",
11 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
12 | )
13 | async def client(lsp_client: LanguageClient):
14 | # Setup
15 | params = types.InitializeParams(capabilities=types.ClientCapabilities())
16 | await lsp_client.initialize_session(params)
17 |
18 | yield
19 |
20 | # Teardown
21 | await lsp_client.shutdown_session()
22 |
23 |
24 | @pytest.mark.asyncio(loop_scope="module")
25 | async def test_completion_hello(client: LanguageClient):
26 | """Ensure that the server implements completions correctly."""
27 |
28 | results = await client.text_document_completion_async(
29 | params=types.CompletionParams(
30 | position=types.Position(line=1, character=0),
31 | text_document=types.TextDocumentIdentifier(uri="file:///path/to/file.txt"),
32 | )
33 | )
34 | assert results is not None
35 |
36 | if isinstance(results, types.CompletionList):
37 | items = results.items
38 | else:
39 | items = results
40 |
41 | labels = {item.label for item in items}
42 | assert "hello" in labels
43 |
44 |
45 | @pytest.mark.asyncio(loop_scope="module")
46 | async def test_completion_world(client: LanguageClient):
47 | """Ensure that the server implements completions correctly."""
48 |
49 | results = await client.text_document_completion_async(
50 | params=types.CompletionParams(
51 | position=types.Position(line=1, character=0),
52 | text_document=types.TextDocumentIdentifier(uri="file:///path/to/file.txt"),
53 | )
54 | )
55 | assert results is not None
56 |
57 | if isinstance(results, types.CompletionList):
58 | items = results.items
59 | else:
60 | items = results
61 |
62 | labels = {item.label for item in items}
63 | assert "world" in labels
64 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/generic-rpc/server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from pygls.protocol import JsonRPCProtocol, default_converter
4 | from pygls.server import JsonRPCServer
5 |
6 | server = JsonRPCServer(
7 | protocol_cls=JsonRPCProtocol, converter_factory=default_converter
8 | )
9 |
10 |
11 | @server.feature("math/add")
12 | def addition(ls: JsonRPCServer, params):
13 | a = params.a
14 | b = params.b
15 |
16 | ls.protocol.notify("log/message", dict(message=f"{a=}"))
17 | ls.protocol.notify("log/message", dict(message=f"{b=}"))
18 |
19 | return dict(total=a + b)
20 |
21 |
22 | @server.feature("math/sub")
23 | def subtraction(ls: JsonRPCServer, params):
24 | a = params.a
25 | b = params.b
26 |
27 | ls.protocol.notify("log/message", dict(message=f"{a=}"))
28 | ls.protocol.notify("log/message", dict(message=f"{b=}"))
29 |
30 | return dict(total=b - a)
31 |
32 |
33 | @server.feature("server/exit")
34 | def server_exit(ls: JsonRPCServer, params):
35 | sys.exit(0)
36 |
37 |
38 | if __name__ == "__main__":
39 | server.start_io()
40 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/generic-rpc/t_server.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 |
4 | import pytest
5 | import pytest_lsp
6 | from pygls.client import JsonRPCClient
7 | from pytest_lsp import ClientServerConfig
8 |
9 |
10 | def client_factory():
11 | client = JsonRPCClient()
12 |
13 | @client.feature("log/message")
14 | def _on_message(params):
15 | logging.info("LOG: %s", params.message)
16 |
17 | return client
18 |
19 |
20 | @pytest_lsp.fixture(
21 | config=ClientServerConfig(
22 | client_factory=client_factory, server_command=[sys.executable, "server.py"]
23 | ),
24 | )
25 | async def client(rpc_client: JsonRPCClient):
26 | # Setup code here (if any)
27 |
28 | yield
29 |
30 | # Teardown code here
31 | rpc_client.protocol.notify("server/exit", {})
32 |
33 |
34 | @pytest.mark.asyncio
35 | async def test_add(client: JsonRPCClient):
36 | """Ensure that the server implements addition correctly."""
37 |
38 | result = await client.protocol.send_request_async(
39 | "math/add", params={"a": 1, "b": 2}
40 | )
41 | assert result.total == 3
42 |
43 |
44 | @pytest.mark.asyncio
45 | async def test_sub(client: JsonRPCClient):
46 | """Ensure that the server implements addition correctly."""
47 |
48 | result = await client.protocol.send_request_async(
49 | "math/sub", params={"a": 1, "b": 2}
50 | )
51 | assert result.total == -1
52 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/getting-started-fail/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol.types import TEXT_DOCUMENT_COMPLETION, CompletionItem, CompletionParams
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("hello-world", "v1")
5 |
6 |
7 | @server.feature(TEXT_DOCUMENT_COMPLETION)
8 | def completion(ls: LanguageServer, params: CompletionParams):
9 | return [
10 | CompletionItem(label="hello"),
11 | CompletionItem(label="world"),
12 | ]
13 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/getting-started-fail/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest_lsp
4 | from lsprotocol.types import (
5 | ClientCapabilities,
6 | CompletionList,
7 | CompletionParams,
8 | InitializeParams,
9 | Position,
10 | TextDocumentIdentifier,
11 | )
12 | from pytest_lsp import ClientServerConfig, LanguageClient
13 |
14 |
15 | @pytest_lsp.fixture(
16 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
17 | )
18 | async def client(lsp_client: LanguageClient):
19 | # Setup
20 | params = InitializeParams(capabilities=ClientCapabilities())
21 | await lsp_client.initialize_session(params)
22 |
23 | yield
24 |
25 | # Teardown
26 | await lsp_client.shutdown_session()
27 |
28 |
29 | async def test_completions(client: LanguageClient):
30 | """Ensure that the server implements completions correctly."""
31 |
32 | results = await client.text_document_completion_async(
33 | params=CompletionParams(
34 | position=Position(line=1, character=0),
35 | text_document=TextDocumentIdentifier(uri="file:///path/to/file.txt"),
36 | )
37 | )
38 |
39 | assert results is not None
40 |
41 | if isinstance(results, CompletionList):
42 | items = results.items
43 | else:
44 | items = results
45 |
46 | labels = [item.label for item in items]
47 | assert labels == ["hello", "world"]
48 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/getting-started/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol.types import TEXT_DOCUMENT_COMPLETION, CompletionItem, CompletionParams
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("hello-world", "v1")
5 |
6 |
7 | @server.feature(TEXT_DOCUMENT_COMPLETION)
8 | def completion(ls: LanguageServer, params: CompletionParams):
9 | return [
10 | CompletionItem(label="hello"),
11 | CompletionItem(label="world"),
12 | ]
13 |
14 |
15 | if __name__ == "__main__":
16 | server.start_io()
17 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/getting-started/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol.types import (
6 | ClientCapabilities,
7 | CompletionList,
8 | CompletionParams,
9 | InitializeParams,
10 | Position,
11 | TextDocumentIdentifier,
12 | )
13 | from pytest_lsp import ClientServerConfig, LanguageClient
14 |
15 |
16 | @pytest_lsp.fixture(
17 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
18 | )
19 | async def client(lsp_client: LanguageClient):
20 | # Setup
21 | params = InitializeParams(capabilities=ClientCapabilities())
22 | await lsp_client.initialize_session(params)
23 |
24 | yield
25 |
26 | # Teardown
27 | await lsp_client.shutdown_session()
28 |
29 |
30 | @pytest.mark.asyncio
31 | async def test_completions(client: LanguageClient):
32 | """Ensure that the server implements completions correctly."""
33 |
34 | results = await client.text_document_completion_async(
35 | params=CompletionParams(
36 | position=Position(line=1, character=0),
37 | text_document=TextDocumentIdentifier(uri="file:///path/to/file.txt"),
38 | )
39 | )
40 | assert results is not None
41 |
42 | if isinstance(results, CompletionList):
43 | items = results.items
44 | else:
45 | items = results
46 |
47 | labels = [item.label for item in items]
48 | assert labels == ["hello", "world"]
49 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/parameterised-clients/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol.types import TEXT_DOCUMENT_COMPLETION, CompletionItem, CompletionParams
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("hello-world", "v1")
5 |
6 |
7 | @server.feature(TEXT_DOCUMENT_COMPLETION)
8 | def completion(ls: LanguageServer, params: CompletionParams):
9 | return [
10 | CompletionItem(label="hello"),
11 | CompletionItem(label="world"),
12 | ]
13 |
14 |
15 | if __name__ == "__main__":
16 | server.start_io()
17 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/parameterised-clients/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest_lsp
4 | from lsprotocol.types import (
5 | CompletionList,
6 | CompletionParams,
7 | InitializeParams,
8 | Position,
9 | TextDocumentIdentifier,
10 | )
11 | from pytest_lsp import ClientServerConfig, LanguageClient, client_capabilities
12 |
13 |
14 | @pytest_lsp.fixture(
15 | params=["neovim", "visual_studio_code"],
16 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
17 | )
18 | async def client(request, lsp_client: LanguageClient):
19 | # Setup
20 | params = InitializeParams(capabilities=client_capabilities(request.param))
21 | await lsp_client.initialize_session(params)
22 |
23 | yield
24 |
25 | # Teardown
26 | await lsp_client.shutdown_session()
27 |
28 |
29 | async def test_completions(client: LanguageClient):
30 | """Ensure that the server implements completions correctly."""
31 |
32 | results = await client.text_document_completion_async(
33 | params=CompletionParams(
34 | position=Position(line=1, character=0),
35 | text_document=TextDocumentIdentifier(uri="file:///path/to/file.txt"),
36 | )
37 | )
38 | assert results is not None
39 |
40 | if isinstance(results, CompletionList):
41 | items = results.items
42 | else:
43 | items = results
44 |
45 | labels = [item.label for item in items]
46 | assert labels == ["hello", "world"]
47 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/ruff.toml:
--------------------------------------------------------------------------------
1 | extend = "../../ruff.toml"
2 | src = ["."]
3 |
4 | [lint.isort]
5 | force-single-line = false
6 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/server-stderr/server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from lsprotocol import types
4 | from pygls.lsp.server import LanguageServer
5 |
6 | server = LanguageServer("server-stderr", "v1")
7 |
8 |
9 | @server.feature(types.TEXT_DOCUMENT_COMPLETION)
10 | def completion(params: types.CompletionParams):
11 | items = []
12 |
13 | for i in range(10):
14 | print(f"Suggesting item {i}", file=sys.stderr, flush=True)
15 | items.append(types.CompletionItem(label=f"item-{i}"))
16 |
17 | return items
18 |
19 |
20 | if __name__ == "__main__":
21 | server.start_io()
22 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/server-stderr/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol.types import (
6 | ClientCapabilities,
7 | CompletionList,
8 | CompletionParams,
9 | InitializeParams,
10 | Position,
11 | TextDocumentIdentifier,
12 | )
13 | from pytest_lsp import ClientServerConfig, LanguageClient
14 |
15 |
16 | @pytest_lsp.fixture(
17 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
18 | )
19 | async def client(lsp_client: LanguageClient):
20 | # Setup
21 | params = InitializeParams(capabilities=ClientCapabilities())
22 | await lsp_client.initialize_session(params)
23 |
24 | yield
25 |
26 | # Teardown
27 | await lsp_client.shutdown_session()
28 |
29 |
30 | async def test_completions(client: LanguageClient):
31 | results = await client.text_document_completion_async(
32 | params=CompletionParams(
33 | position=Position(line=1, character=0),
34 | text_document=TextDocumentIdentifier(uri="file:///path/to/file.txt"),
35 | )
36 | )
37 |
38 | assert results is not None
39 |
40 | if isinstance(results, CompletionList):
41 | items = results.items
42 | else:
43 | items = results
44 |
45 | labels = [item.label for item in items]
46 | assert labels == [f"item-{i}" for i in range(10)]
47 | pytest.fail("Failing test!") # Force the test case to fail.
48 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-create-progress/server.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import Mock
2 |
3 | from lsprotocol import types
4 | from pygls.lsp.server import LanguageServer
5 |
6 | server = LanguageServer("window-create-progress", "v1")
7 |
8 |
9 | @server.command("do.progress")
10 | async def do_progress(ls: LanguageServer, *args):
11 | token = "a-token"
12 |
13 | await ls.work_done_progress.create_async(token)
14 |
15 | # Begin
16 | ls.work_done_progress.begin(
17 | token,
18 | types.WorkDoneProgressBegin(title="Indexing", percentage=0),
19 | )
20 | # Report
21 | for i in range(1, 4):
22 | ls.work_done_progress.report(
23 | token,
24 | types.WorkDoneProgressReport(message=f"{i * 25}%", percentage=i * 25),
25 | )
26 | # End
27 | ls.work_done_progress.end(token, types.WorkDoneProgressEnd(message="Finished"))
28 |
29 | return "a result"
30 |
31 |
32 | @server.command("duplicate.progress")
33 | async def duplicate_progress(ls: LanguageServer, *args):
34 | token = "duplicate-token"
35 |
36 | # Need to stop pygls preventing us from using the progress API wrong.
37 | ls.work_done_progress._check_token_registered = Mock()
38 | await ls.work_done_progress.create_async(token)
39 |
40 | # pytest-lsp should return an error here.
41 | await ls.work_done_progress.create_async(token)
42 |
43 |
44 | @server.command("no.progress")
45 | async def no_progress(ls: LanguageServer, *args):
46 | token = "undefined-token"
47 |
48 | # Begin
49 | ls.work_done_progress.begin(
50 | token,
51 | types.WorkDoneProgressBegin(title="Indexing", percentage=0, cancellable=False),
52 | )
53 | # Report
54 | for i in range(1, 4):
55 | ls.work_done_progress.report(
56 | token,
57 | types.WorkDoneProgressReport(message=f"{i * 25}%", percentage=i * 25),
58 | )
59 | # End
60 | ls.work_done_progress.end(token, types.WorkDoneProgressEnd(message="Finished"))
61 |
62 |
63 | if __name__ == "__main__":
64 | server.start_io()
65 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-create-progress/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol import types
6 | from pytest_lsp import ClientServerConfig, LanguageClient, LspSpecificationWarning
7 |
8 |
9 | @pytest_lsp.fixture(
10 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
11 | )
12 | async def client(lsp_client: LanguageClient):
13 | # Setup
14 | params = types.InitializeParams(capabilities=types.ClientCapabilities())
15 | await lsp_client.initialize_session(params)
16 |
17 | yield
18 |
19 | # Teardown
20 | await lsp_client.shutdown_session()
21 |
22 |
23 | @pytest.mark.asyncio
24 | async def test_progress(client: LanguageClient):
25 | result = await client.workspace_execute_command_async(
26 | params=types.ExecuteCommandParams(command="do.progress")
27 | )
28 |
29 | assert result == "a result"
30 |
31 | progress = client.progress_reports["a-token"]
32 | assert progress == [
33 | types.WorkDoneProgressBegin(title="Indexing", percentage=0),
34 | types.WorkDoneProgressReport(message="25%", percentage=25),
35 | types.WorkDoneProgressReport(message="50%", percentage=50),
36 | types.WorkDoneProgressReport(message="75%", percentage=75),
37 | types.WorkDoneProgressEnd(message="Finished"),
38 | ]
39 |
40 |
41 | @pytest.mark.asyncio
42 | async def test_duplicate_progress(client: LanguageClient):
43 | with pytest.warns(
44 | LspSpecificationWarning, match="Duplicate progress token: 'duplicate-token'"
45 | ):
46 | await client.workspace_execute_command_async(
47 | params=types.ExecuteCommandParams(command="duplicate.progress")
48 | )
49 |
50 |
51 | @pytest.mark.asyncio
52 | async def test_unknown_progress(client: LanguageClient):
53 | with pytest.warns(
54 | LspSpecificationWarning, match="Unknown progress token: 'undefined-token'"
55 | ):
56 | await client.workspace_execute_command_async(
57 | params=types.ExecuteCommandParams(command="no.progress")
58 | )
59 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-log-message-fail/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol import types
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("window-log-message", "v1")
5 |
6 |
7 | @server.feature(types.TEXT_DOCUMENT_COMPLETION)
8 | def completion(ls: LanguageServer, params: types.CompletionParams):
9 | items = []
10 |
11 | for i in range(10):
12 | ls.window_log_message(
13 | types.LogMessageParams(
14 | message=f"Suggesting item {i}",
15 | type=types.MessageType.Log,
16 | )
17 | )
18 | items.append(types.CompletionItem(label=f"item-{i}"))
19 |
20 | return items
21 |
22 |
23 | if __name__ == "__main__":
24 | server.start_io()
25 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-log-message-fail/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol.types import (
6 | ClientCapabilities,
7 | CompletionList,
8 | CompletionParams,
9 | InitializeParams,
10 | Position,
11 | TextDocumentIdentifier,
12 | )
13 | from pytest_lsp import ClientServerConfig, LanguageClient
14 |
15 |
16 | @pytest_lsp.fixture(
17 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
18 | )
19 | async def client(lsp_client: LanguageClient):
20 | # Setup
21 | params = InitializeParams(capabilities=ClientCapabilities())
22 | await lsp_client.initialize_session(params)
23 |
24 | yield
25 |
26 | # Teardown
27 | await lsp_client.shutdown_session()
28 |
29 |
30 | async def test_completions(client: LanguageClient):
31 | results = await client.text_document_completion_async(
32 | params=CompletionParams(
33 | position=Position(line=1, character=0),
34 | text_document=TextDocumentIdentifier(uri="file:///path/to/file.txt"),
35 | )
36 | )
37 |
38 | assert results is not None
39 |
40 | if isinstance(results, CompletionList):
41 | items = results.items
42 | else:
43 | items = results
44 |
45 | labels = [item.label for item in items]
46 | assert labels == [f"item-{i}" for i in range(10)]
47 |
48 | for idx, log_message in enumerate(client.log_messages):
49 | assert log_message.message == f"Suggesting item {idx}"
50 |
51 | pytest.fail("Failing test!") # Force the test case to fail.
52 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-log-message/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol import types
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("window-log-message", "v1")
5 |
6 |
7 | @server.feature(types.TEXT_DOCUMENT_COMPLETION)
8 | def completion(ls: LanguageServer, params: types.CompletionParams):
9 | items = []
10 |
11 | for i in range(10):
12 | ls.window_log_message(
13 | types.LogMessageParams(
14 | message=f"Suggesting item {i}", type=types.MessageType.Log
15 | )
16 | )
17 | items.append(types.CompletionItem(label=f"item-{i}"))
18 |
19 | return items
20 |
21 |
22 | if __name__ == "__main__":
23 | server.start_io()
24 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-log-message/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol.types import (
6 | ClientCapabilities,
7 | CompletionList,
8 | CompletionParams,
9 | InitializeParams,
10 | Position,
11 | TextDocumentIdentifier,
12 | )
13 | from pytest_lsp import ClientServerConfig, LanguageClient
14 |
15 |
16 | @pytest_lsp.fixture(
17 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
18 | )
19 | async def client(lsp_client: LanguageClient):
20 | # Setup
21 | params = InitializeParams(capabilities=ClientCapabilities())
22 | await lsp_client.initialize_session(params)
23 |
24 | yield
25 |
26 | # Teardown
27 | await lsp_client.shutdown_session()
28 |
29 |
30 | @pytest.mark.asyncio
31 | async def test_completions(client: LanguageClient):
32 | results = await client.text_document_completion_async(
33 | params=CompletionParams(
34 | position=Position(line=1, character=0),
35 | text_document=TextDocumentIdentifier(uri="file:///path/to/file.txt"),
36 | )
37 | )
38 |
39 | assert results is not None
40 |
41 | if isinstance(results, CompletionList):
42 | items = results.items
43 | else:
44 | items = results
45 |
46 | labels = [item.label for item in items]
47 | assert labels == [f"item-{i}" for i in range(10)]
48 |
49 | for idx, log_message in enumerate(client.log_messages):
50 | assert log_message.message == f"Suggesting item {idx}"
51 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-show-document/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol import types
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("window-show-document", "v1")
5 |
6 |
7 | @server.feature(types.TEXT_DOCUMENT_COMPLETION)
8 | async def completion(ls: LanguageServer, params: types.CompletionParams):
9 | items = []
10 | await ls.window_show_document_async(
11 | types.ShowDocumentParams(uri=params.text_document.uri)
12 | )
13 |
14 | for i in range(10):
15 | items.append(types.CompletionItem(label=f"item-{i}"))
16 |
17 | return items
18 |
19 |
20 | if __name__ == "__main__":
21 | server.start_io()
22 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-show-document/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol.types import (
6 | ClientCapabilities,
7 | CompletionList,
8 | CompletionParams,
9 | InitializeParams,
10 | Position,
11 | TextDocumentIdentifier,
12 | )
13 | from pytest_lsp import ClientServerConfig, LanguageClient
14 |
15 |
16 | @pytest_lsp.fixture(
17 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
18 | )
19 | async def client(lsp_client: LanguageClient):
20 | # Setup
21 | params = InitializeParams(capabilities=ClientCapabilities())
22 | await lsp_client.initialize_session(params)
23 |
24 | yield
25 |
26 | # Teardown
27 | await lsp_client.shutdown_session()
28 |
29 |
30 | @pytest.mark.asyncio
31 | async def test_completions(client: LanguageClient):
32 | test_uri = "file:///path/to/file.txt"
33 | results = await client.text_document_completion_async(
34 | params=CompletionParams(
35 | position=Position(line=1, character=0),
36 | text_document=TextDocumentIdentifier(uri=test_uri),
37 | )
38 | )
39 |
40 | assert results is not None
41 |
42 | if isinstance(results, CompletionList):
43 | items = results.items
44 | else:
45 | items = results
46 |
47 | labels = [item.label for item in items]
48 | assert labels == [f"item-{i}" for i in range(10)]
49 |
50 | assert client.shown_documents[0].uri == test_uri
51 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-show-message/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol import types
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("window-show-message", "v1")
5 |
6 |
7 | @server.feature(types.TEXT_DOCUMENT_COMPLETION)
8 | def completion(ls: LanguageServer, params: types.CompletionParams):
9 | items = []
10 |
11 | for i in range(10):
12 | ls.window_show_message(
13 | types.ShowMessageParams(
14 | message=f"Suggesting item {i}",
15 | type=types.MessageType.Log,
16 | )
17 | )
18 | items.append(types.CompletionItem(label=f"item-{i}"))
19 |
20 | return items
21 |
22 |
23 | if __name__ == "__main__":
24 | server.start_io()
25 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/window-show-message/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol.types import (
6 | ClientCapabilities,
7 | CompletionList,
8 | CompletionParams,
9 | InitializeParams,
10 | Position,
11 | TextDocumentIdentifier,
12 | )
13 | from pytest_lsp import ClientServerConfig, LanguageClient
14 |
15 |
16 | @pytest_lsp.fixture(
17 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
18 | )
19 | async def client(lsp_client: LanguageClient):
20 | # Setup
21 | params = InitializeParams(capabilities=ClientCapabilities())
22 | await lsp_client.initialize_session(params)
23 |
24 | yield
25 |
26 | # Teardown
27 | await lsp_client.shutdown_session()
28 |
29 |
30 | @pytest.mark.asyncio
31 | async def test_completions(client: LanguageClient):
32 | results = await client.text_document_completion_async(
33 | params=CompletionParams(
34 | position=Position(line=1, character=0),
35 | text_document=TextDocumentIdentifier(uri="file:///path/to/file.txt"),
36 | )
37 | )
38 |
39 | assert results is not None
40 |
41 | if isinstance(results, CompletionList):
42 | items = results.items
43 | else:
44 | items = results
45 |
46 | labels = [item.label for item in items]
47 | assert labels == [f"item-{i}" for i in range(10)]
48 |
49 | for idx, shown_message in enumerate(client.messages):
50 | assert shown_message.message == f"Suggesting item {idx}"
51 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/workspace-configuration/server.py:
--------------------------------------------------------------------------------
1 | from lsprotocol import types
2 | from pygls.lsp.server import LanguageServer
3 |
4 | server = LanguageServer("workspace-configuration", "v1")
5 |
6 |
7 | @server.command("server.configuration")
8 | async def configuration(ls: LanguageServer, *args):
9 | results = await ls.workspace_configuration_async(
10 | types.ConfigurationParams(
11 | items=[
12 | types.ConfigurationItem(scope_uri="file://workspace/file.txt"),
13 | types.ConfigurationItem(section="not.found"),
14 | types.ConfigurationItem(section="values.c"),
15 | ]
16 | )
17 | )
18 |
19 | a = results[0]["values"]["a"]
20 | assert results[1] is None
21 | c = results[2]
22 |
23 | return a + c
24 |
25 |
26 | if __name__ == "__main__":
27 | server.start_io()
28 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/examples/workspace-configuration/t_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 | import pytest_lsp
5 | from lsprotocol import types
6 | from pytest_lsp import ClientServerConfig, LanguageClient
7 |
8 |
9 | @pytest_lsp.fixture(
10 | config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
11 | )
12 | async def client(lsp_client: LanguageClient):
13 | # Setup
14 | params = types.InitializeParams(
15 | capabilities=types.ClientCapabilities(
16 | workspace=types.WorkspaceClientCapabilities(configuration=False)
17 | )
18 | )
19 | await lsp_client.initialize_session(params)
20 |
21 | yield
22 |
23 | # Teardown
24 | await lsp_client.shutdown_session()
25 |
26 |
27 | @pytest.mark.asyncio
28 | async def test_configuration(client: LanguageClient):
29 | global_config = {"values": {"a": 42, "c": 4}}
30 |
31 | workspace_uri = "file://workspace/file.txt"
32 | workspace_config = {"a": 1, "c": 1}
33 |
34 | client.set_configuration(global_config)
35 | client.set_configuration(
36 | workspace_config, section="values", scope_uri=workspace_uri
37 | )
38 |
39 | result = await client.workspace_execute_command_async(
40 | params=types.ExecuteCommandParams(command="server.configuration")
41 | )
42 | assert result == 5
43 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/servers/capabilities.py:
--------------------------------------------------------------------------------
1 | from lsprotocol.converters import get_converter
2 | from pygls.lsp.server import LanguageServer
3 |
4 | converter = get_converter()
5 | server = LanguageServer(name="capabilities-server", version="v1.0")
6 |
7 |
8 | @server.command("return.client.capabilities")
9 | def on_initialize(ls: LanguageServer, *args):
10 | return ls.client_capabilities
11 |
12 |
13 | if __name__ == "__main__":
14 | server.start_io()
15 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/servers/completion_exit.py:
--------------------------------------------------------------------------------
1 | # A server that exits mid request.
2 | import sys
3 |
4 | from lsprotocol import types
5 | from pygls.lsp.server import LanguageServer
6 |
7 |
8 | class CountingLanguageServer(LanguageServer):
9 | count: int = 0
10 |
11 |
12 | server = CountingLanguageServer(name="completion-exit-server", version="v1.0")
13 |
14 |
15 | @server.feature(types.TEXT_DOCUMENT_COMPLETION)
16 | def on_complete(server: CountingLanguageServer, params: types.CompletionParams):
17 | server.count += 1
18 | if server.count == 5:
19 | sys.exit(0)
20 |
21 | return [types.CompletionItem(label=f"{server.count}")]
22 |
23 |
24 | if __name__ == "__main__":
25 | server.start_io()
26 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/servers/crash.py:
--------------------------------------------------------------------------------
1 | # Not a server, used to simulate the case where the server crashes before
2 | # it can boot.
3 |
4 |
5 | def f(x):
6 | return x / 0
7 |
8 |
9 | def g(x):
10 | return f(x) * f(x)
11 |
12 |
13 | def h(x):
14 | return g(x) - f(x)
15 |
16 |
17 | h(2)
18 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/servers/hello.py:
--------------------------------------------------------------------------------
1 | # Not actually a server, but a script that prints hello world.
2 | print("Hello, world!")
3 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/servers/invalid_json.py:
--------------------------------------------------------------------------------
1 | # A server that returns a message that cannot be parsed as JSON.
2 | import json
3 | import sys
4 |
5 | from lsprotocol import types
6 | from pygls.io_ import StdoutWriter
7 | from pygls.lsp.server import LanguageServer
8 |
9 | server = LanguageServer(name="completion-exit-server", version="v1.0")
10 |
11 |
12 | def bad_send_data(data):
13 | """Sends data to the client in a way that cannot be parsed."""
14 | if not data:
15 | return
16 |
17 | self = server.protocol
18 | body = json.dumps(data, default=self._serialize_message)
19 | body = body.replace('"', "'").encode(self.CHARSET)
20 | header = (
21 | f"Content-Length: {len(body)}\r\n"
22 | f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n"
23 | ).encode(self.CHARSET)
24 |
25 | self.writer.write(header + body)
26 |
27 |
28 | @server.feature(types.TEXT_DOCUMENT_COMPLETION)
29 | def on_complete(server: LanguageServer, params: types.CompletionParams):
30 | server.protocol._send_data = bad_send_data
31 | server.protocol.set_writer(StdoutWriter(sys.stdout.buffer))
32 |
33 | return [types.CompletionItem(label="item-one")]
34 |
35 |
36 | if __name__ == "__main__":
37 | server.start_io()
38 |
--------------------------------------------------------------------------------
/lib/pytest-lsp/tests/servers/notify_exit.py:
--------------------------------------------------------------------------------
1 | # A server that exits mid request.
2 | import sys
3 |
4 | from lsprotocol import types
5 | from pygls.lsp.server import LanguageServer
6 |
7 |
8 | class CountingLanguageServer(LanguageServer):
9 | count: int = 0
10 |
11 |
12 | server = CountingLanguageServer(name="completion-exit-server", version="v1.0")
13 |
14 |
15 | @server.feature("server/exit")
16 | def server_exit(*args):
17 | sys.exit(0)
18 |
19 |
20 | @server.feature(types.TEXT_DOCUMENT_COMPLETION)
21 | def on_complete(server: CountingLanguageServer, params: types.CompletionParams):
22 | server.count += 1
23 | if server.count == 5:
24 | sys.exit(0)
25 |
26 | return [types.CompletionItem(label=f"{server.count}")]
27 |
28 |
29 | if __name__ == "__main__":
30 | server.start_io()
31 |
--------------------------------------------------------------------------------
/scripts/check_capabilities.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pathlib
3 | import sys
4 |
5 | from lsprotocol import types
6 | from lsprotocol.converters import get_converter
7 |
8 |
9 | def check(filepath: pathlib.Path, converter):
10 | obj = json.loads(filepath.read_text())
11 |
12 | try:
13 | converter.structure(obj, types.InitializeParams)
14 | except Exception as e:
15 | print(f"{filepath.name}: {e}")
16 | return 1
17 |
18 | filepath.write_text(json.dumps(obj, indent=2))
19 | return 0
20 |
21 |
22 | def main(files: list[str]):
23 | converter = get_converter()
24 |
25 | total = 0
26 | for filename in files:
27 | total += check(pathlib.Path(filename), converter)
28 |
29 | return total
30 |
31 |
32 | if __name__ == "__main__":
33 | sys.exit(main(sys.argv[1:]))
34 |
--------------------------------------------------------------------------------
/scripts/nvim-capabilities.lua:
--------------------------------------------------------------------------------
1 | -- Dump the client capabilities for this version of neovim.
2 | local version_info = vim.version()
3 | local version = string.format('%d.%d.%d', version_info.major, version_info.minor, version_info.patch)
4 |
5 | local params = {
6 | clientInfo = {
7 | name = 'Neovim',
8 | version = version,
9 | },
10 | capabilities = vim.lsp.protocol.make_client_capabilities(),
11 | }
12 |
13 | local json = vim.json.encode(params)
14 | local bufnr = vim.api.nvim_create_buf(true, false)
15 | vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { json })
16 |
17 | vim.api.nvim_buf_call(bufnr, function()
18 | vim.cmd(string.format(':w neovim_v%s.json', version))
19 | vim.cmd('.!python -m json.tool %')
20 | vim.cmd(':w')
21 | end)
22 |
--------------------------------------------------------------------------------
/scripts/should-build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Script to check if we should build a given component or not.
3 |
4 | # File patterns to check for each component, if there's a match a build will be
5 | # triggered
6 | LSP_DEVTOOLS="^lib/lsp-devtools/"
7 | PYTEST_LSP="^lib/pytest-lsp/"
8 |
9 | # Determine which files have changed
10 | git diff --name-only ${BASE}..HEAD -- >> changes
11 | echo -e "Files Changed:\n"
12 | cat changes
13 |
14 | case $1 in
15 | lsp-devtools)
16 | PATTERN=${LSP_DEVTOOLS}
17 | ;;
18 | pytest-lsp)
19 | PATTERN=${PYTEST_LSP}
20 | ;;
21 | *)
22 | echo "Unknown component ${1}"
23 | exit 1
24 | ;;
25 | esac
26 |
27 | changes=$(grep -E "${PATTERN}" changes)
28 | echo
29 |
30 | rm changes
31 |
32 | if [ -z "$changes" ]; then
33 | echo "There is nothing to do."
34 | else
35 | echo "Changes detected, doing build!"
36 | echo "build::true" >> $GITHUB_OUTPUT
37 | fi
38 |
--------------------------------------------------------------------------------