diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..1a630a709 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,41 @@ +name: Bug Report +description: Report broken or unexpected behavior +labels: [bug, triage] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to report this! + - type: textarea + id: expected + attributes: + label: Expected behavior + description: Tell us what you expected to happen. + placeholder: Tell us what you expected to happen. + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: Tell us what happened instead + placeholder: Tell us what happened instead + validations: + required: true + - type: textarea + id: logs + attributes: + label: Log output + description: | + Copy and paste any relevant log output here (most issues will be difficult to diagnose without this!). + For best results please make sure that debug logging is enabled (`esbonio.logging.level = debug`) + This will be automatically formatted as code, so no need for backticks. + render: shell + - type: textarea + id: conf + attributes: + label: (Optional) Settings from conf.py + description: | + If you think any settings from your project's `conf.py` are applicable, feel free to include them here + This will be automatically formatted as code, so no need for backticks. + render: python diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..2c6980fea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,16 @@ +name: Feature Request +description: Suggest an idea for this project +labels: [enhancement, triage] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to fill this out! + - type: textarea + id: solution + attributes: + label: What would you like to see + description: A clear and concise description of what you want to happen. + placeholder: A clear and concise description of what you want to happen. + validations: + required: true diff --git a/.github/workflows/lsp-pr.yml b/.github/workflows/lsp-pr.yml index f95e5d47a..785c8cd50 100644 --- a/.github/workflows/lsp-pr.yml +++ b/.github/workflows/lsp-pr.yml @@ -19,7 +19,7 @@ jobs: python-version: "3.11" - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-lsp-pr-pip-deps-3.11 @@ -57,7 +57,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-lsp-pr-pip-deps-${{ matrix.python-version }} diff --git a/.github/workflows/sphinx-ext-pr.yml b/.github/workflows/sphinx-ext-pr.yml index ebbb1135e..e29d304f7 100644 --- a/.github/workflows/sphinx-ext-pr.yml +++ b/.github/workflows/sphinx-ext-pr.yml @@ -19,7 +19,7 @@ jobs: python-version: "3.11" - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-ext-pr-pip-deps-3.11 @@ -59,7 +59,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-ext-pr-pip-deps-${{ matrix.python-version }} diff --git a/.github/workflows/vscode-pr.yml b/.github/workflows/vscode-pr.yml index a48dd0ffb..1b3844683 100644 --- a/.github/workflows/vscode-pr.yml +++ b/.github/workflows/vscode-pr.yml @@ -25,7 +25,7 @@ jobs: python-version: "3.8" - name: Pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-vscode-pip-deps-${{ hashFiles('code/requirements.txt') }} diff --git a/.github/workflows/vscode-release.yml b/.github/workflows/vscode-release.yml index 8e4d794a3..88fbdc744 100644 --- a/.github/workflows/vscode-release.yml +++ b/.github/workflows/vscode-release.yml @@ -7,11 +7,8 @@ on: - 'code/**' jobs: - release: - name: vscode release + build: runs-on: ubuntu-latest - environment: - name: vscode-marketplace steps: - uses: 'actions/checkout@v4' @@ -27,7 +24,7 @@ jobs: python-version: "3.8" - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-vscode-pip-deps-${{ hashFiles('code/requirements.txt') }} @@ -63,13 +60,6 @@ jobs: path: code/*.vsix if-no-files-found: error - - name: 'Publish Extension' - run: | - cd code - npm run deploy - env: - VSCE_PAT: ${{ secrets.VSCODE_PAT }} - - name: Create Release run: | gh release create "${RELEASE_TAG}" \ @@ -78,3 +68,61 @@ jobs: ./code/*.vsix env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + marketplace-release: + name: vscode release + needs: build + runs-on: ubuntu-latest + environment: + name: vscode-marketplace + steps: + - uses: 'actions/checkout@v4' + + - uses: 'actions/setup-node@v4' + with: + node-version: 18.x + cache: 'npm' + cache-dependency-path: 'code/package-lock.json' + + - uses: actions/download-artifact@v4 + name: 'Download Extension' + with: + name: 'vsix' + path: code + + - name: 'Publish Extension' + run: | + cd code + npm ci --prefer-offline + npm run deploy-vsce + env: + VSCE_PAT: ${{ secrets.VSCODE_PAT }} + + open-vsx-release: + name: open vsx release + needs: build + runs-on: ubuntu-latest + environment: + name: open-vsx + steps: + - uses: 'actions/checkout@v4' + + - uses: 'actions/setup-node@v4' + with: + node-version: 18.x + cache: 'npm' + cache-dependency-path: 'code/package-lock.json' + + - uses: actions/download-artifact@v4 + name: 'Download Extension' + with: + name: 'vsix' + path: code + + - name: 'Publish Extension' + run: | + cd code + npm ci --prefer-offline + npm run deploy-ovsx + env: + OVSX_PAT: ${{ secrets.OVSX_PAT }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d9eb52611..25e0683f6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,14 +2,14 @@ exclude: '.bumpversion.cfg$' repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.4.0 hooks: - id: black @@ -28,7 +28,7 @@ repos: args: [--settings-file=lib/esbonio/pyproject.toml] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.8.0' + rev: 'v1.9.0' hooks: - id: mypy name: mypy (scripts) diff --git a/.vscode/settings.json b/.vscode/settings.json index af9a8a575..13097473d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,13 +31,11 @@ "**/.vscode-test": true, "**/.mypy_cache": true }, - "esbonio.sphinx.buildDir": "${confDir}/_build", "esbonio.server.showDeprecationWarnings": true, "isort.args": [ "--settings-file", "./lib/esbonio/pyproject.toml" ], - "python.defaultInterpreterPath": "${workspaceRoot}/.env/bin/python", "python.testing.pytestArgs": [ "lib/esbonio/tests" ], diff --git a/README.md b/README.md index 0bc5a7258..2f468007c 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,16 @@ **esbonio - (v.) to explain** -Esbonio aims to make it easier to work with [reStructuredText](https://docutils.sourceforge.io/rst.html) tools such as [Sphinx](https://www.sphinx-doc.org/en/master/) by providing a [Language Server](https://langserver.org/) to enhance your editing experience. +[reStructuredText]: https://docutils.sourceforge.io/rst.html +[Sphinx]: https://www.sphinx-doc.org/en/master/ +[Language Server]: https://langserver.org/ + +Esbonio aims to make it easier to work with [reStructuredText] tools such as [Sphinx] by providing a [Language Server] to enhance your editing experience. The Esbonio project is made up from a number of sub-projects + + ## `lib/esbonio/` - A Language Server for Sphinx projects. + [![PyPI](https://img.shields.io/pypi/v/esbonio?style=flat-square)![PyPI - Downloads](https://img.shields.io/pypi/dm/esbonio?style=flat-square)](https://pypistats.org/packages/esbonio)[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/swyddfa/esbonio/blob/develop/lib/esbonio/LICENSE) The language server provides the following features. @@ -59,6 +66,7 @@ The language server provides the following features. ## `code/` - A VSCode extension for editing Sphinx projects + [![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/swyddfa.esbonio?style=flat-square)![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/swyddfa.esbonio?style=flat-square)![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/swyddfa.esbonio?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=swyddfa.esbonio)[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/swyddfa/esbonio/blob/develop/code/LICENSE)

@@ -69,8 +77,13 @@ This extension is purely focused on bringing the `esbonio` language server into ### You're probably looking for the reStructuredText Extension -You may already be familiar with the [reStructuredText](https://marketplace.visualstudio.com/items?itemName=lextudio.restructuredtext) extension which, as of [v171.0.0](https://github.com/vscode-restructuredtext/vscode-restructuredtext/releases/tag/171.0.0) now also integrates the `esbonio` language server into VSCode. -It also integrates other tools such as the linters [`doc8`](https://pypi.org/project/doc8/) and [`rstcheck`](https://pypi.org/project/rstcheck/) and provides additional editor functionality making it easier to work with reStructuredText in general. +[reStructuredText extension]: https://marketplace.visualstudio.com/items?itemName=lextudio.restructuredtext +[v171.0.0]: https://github.com/vscode-restructuredtext/vscode-restructuredtext/releases/tag/171.0.0 +[`doc8`]: https://pypi.org/project/doc8/ +[`rstcheck`]: https://pypi.org/project/rstcheck/ + +You may already be familiar with the [reStructuredText extension] which, as of [v171.0.0] now also integrates the `esbonio` language server into VSCode. +It also integrates other tools such as the linters [`doc8`] and [`rstcheck`] and provides additional editor functionality making it easier to work with reStructuredText in general. **Wait.. so why does the Esbonio VSCode extension still exist?** @@ -90,5 +103,7 @@ Try the Esbonio extension if - You want to make use of the newer features available in recent VSCode versions - You are only interested in the features provided by the language server + ## `lib/esbonio-extensions/` - A collection of Sphinx extensions + [![PyPI](https://img.shields.io/pypi/v/esbonio-extensions?style=flat-square)![PyPI - Downloads](https://img.shields.io/pypi/dm/esbonio-extensions?style=flat-square)](https://pypistats.org/packages/esbonio-extensions)[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/swyddfa/esbonio/blob/develop/lib/esbonio-extensions/LICENSE) diff --git a/code/changes/703.fix.md b/code/changes/703.fix.md new file mode 100644 index 000000000..902c1cea1 --- /dev/null +++ b/code/changes/703.fix.md @@ -0,0 +1 @@ +The extension will now notify the server when the user changes Python environment via the Python extension diff --git a/code/changes/748.breaking.md b/code/changes/748.breaking.md new file mode 100644 index 000000000..3327c279c --- /dev/null +++ b/code/changes/748.breaking.md @@ -0,0 +1,2 @@ +- Removed the `esbonio.server.logLevel` option, use `esbonio.logging.level` instead. +- Removed the `esbonio.server.logFilter` option, it has been made obselete by the other `esbonio.logging.*` options diff --git a/code/changes/748.enhancement.md b/code/changes/748.enhancement.md new file mode 100644 index 000000000..695f2c93d --- /dev/null +++ b/code/changes/748.enhancement.md @@ -0,0 +1,9 @@ +Added the following configuration options + +- `esbonio:config:: esbonio.logging.level`, set the default logging level of the server +- `esbonio:config:: esbonio.logging.format`, set the default format of server log messages +- `esbonio:config:: esbonio.logging.filepath`, enable logging to a file +- `esbonio:config:: esbonio.logging.stderr`, print log messages to stderr +- `esbonio:config:: esbonio.logging.window`, send log messages as `window/logMessage` notifications +- `esbonio:config:: esbonio.logging.config`, override logging configuration for individual loggers, see the [documentation](https://docs.esbon.io/en/latest/lsp/reference/configuration.html#lsp-configuration-logging) for details +- `esbonio.trace.server` enables the logging of LSP messages sent to/from the server diff --git a/code/changes/756.breaking.md b/code/changes/756.breaking.md new file mode 100644 index 000000000..e24b6aae1 --- /dev/null +++ b/code/changes/756.breaking.md @@ -0,0 +1 @@ +The `esbonio.server.enabledInPyFiles` configuration option has been removed, use `esbonio.server.documentSelector` instead diff --git a/code/changes/756.enhancement.md b/code/changes/756.enhancement.md new file mode 100644 index 000000000..5db22a0a2 --- /dev/null +++ b/code/changes/756.enhancement.md @@ -0,0 +1 @@ +Added the `esbonio.server.documentSelector` option, granting the user fine grained control over which files the server is enabled in. diff --git a/code/package-lock.json b/code/package-lock.json index 4dd73457d..c26b3702f 100644 --- a/code/package-lock.json +++ b/code/package-lock.json @@ -1,34 +1,34 @@ { "name": "esbonio", - "version": "0.91.0", + "version": "0.92.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "esbonio", - "version": "0.91.0", + "version": "0.92.1", "license": "MIT", "dependencies": { "@vscode/python-extension": "^1.0.5", - "semver": "^7.5.4", + "semver": "^7.6.0", "vscode-languageclient": "^9.0.1" }, "devDependencies": { "@types/glob": "^8.1.0", "@types/node": "^18", "@types/vscode": "1.78.0", - "@vscode/vsce": "^2.22.0", - "esbuild": "^0.19.11", - "typescript": "^5.3.3" + "@vscode/vsce": "^2.25.0", + "esbuild": "^0.20.2", + "typescript": "^5.4.5" }, "engines": { "vscode": "^1.82.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", - "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], @@ -42,9 +42,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", - "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -58,9 +58,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", - "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -74,9 +74,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", - "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -90,9 +90,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", - "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], @@ -106,9 +106,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", - "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -122,9 +122,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", - "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", - "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -154,9 +154,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", - "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -170,9 +170,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", - "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -186,9 +186,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", - "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -202,9 +202,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", - "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -218,9 +218,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", - "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -234,9 +234,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", - "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -250,9 +250,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", - "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -266,9 +266,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", - "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -282,9 +282,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", - "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -298,9 +298,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", - "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -314,9 +314,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", - "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -330,9 +330,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", - "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -346,9 +346,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", - "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -362,9 +362,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", - "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -378,9 +378,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", - "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -431,15 +431,17 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.22.0.tgz", - "integrity": "sha512-8df4uJiM3C6GZ2Sx/KilSKVxsetrTBBIUb3c0W4B1EWHcddioVs5mkyDKtMNP0khP/xBILVSzlXxhV+nm2rC9A==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.25.0.tgz", + "integrity": "sha512-VXMCGUaP6wKBadA7vFQdsksxkBAMoh4ecZgXBwauZMASAgnwYesHyLnqIyWYeRwjy2uEpitHvz/1w5ENnR30pg==", "dev": true, "dependencies": { - "azure-devops-node-api": "^11.0.1", + "azure-devops-node-api": "^12.5.0", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", "commander": "^6.2.1", + "form-data": "^4.0.0", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", @@ -461,7 +463,7 @@ "vsce": "vsce" }, "engines": { - "node": ">= 14" + "node": ">= 16" }, "optionalDependencies": { "keytar": "^7.7.0" @@ -513,10 +515,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/azure-devops-node-api": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", - "integrity": "sha512-YMdjAw9l5p/6leiyIloxj3k7VIvYThKjvqgiQn88r3nhT93ENwsoDS3A83CyJ4uTWzCZ5f5jCi6c27rTU5Pz+A==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, "dependencies": { "tunnel": "0.0.6", @@ -627,13 +635,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -697,6 +711,15 @@ "dev": true, "optional": true }, + "node_modules/cockatiel": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.2.tgz", + "integrity": "sha512-5yARKww0dWyWg2/3xZeXgoxjHLwpVqFptj9Zy7qioJ6+/L0ARM184sgMUrQDjxw7ePJWlGhV998mKhzrxT0/Kg==", + "dev": true, + "engines": { + "node": ">=16" + } + }, "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -722,6 +745,18 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -805,6 +840,32 @@ "node": ">=4.0.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -896,10 +957,31 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", - "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, "bin": { @@ -909,29 +991,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.11", - "@esbuild/android-arm": "0.19.11", - "@esbuild/android-arm64": "0.19.11", - "@esbuild/android-x64": "0.19.11", - "@esbuild/darwin-arm64": "0.19.11", - "@esbuild/darwin-x64": "0.19.11", - "@esbuild/freebsd-arm64": "0.19.11", - "@esbuild/freebsd-x64": "0.19.11", - "@esbuild/linux-arm": "0.19.11", - "@esbuild/linux-arm64": "0.19.11", - "@esbuild/linux-ia32": "0.19.11", - "@esbuild/linux-loong64": "0.19.11", - "@esbuild/linux-mips64el": "0.19.11", - "@esbuild/linux-ppc64": "0.19.11", - "@esbuild/linux-riscv64": "0.19.11", - "@esbuild/linux-s390x": "0.19.11", - "@esbuild/linux-x64": "0.19.11", - "@esbuild/netbsd-x64": "0.19.11", - "@esbuild/openbsd-x64": "0.19.11", - "@esbuild/sunos-x64": "0.19.11", - "@esbuild/win32-arm64": "0.19.11", - "@esbuild/win32-ia32": "0.19.11", - "@esbuild/win32-x64": "0.19.11" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/escape-string-regexp": { @@ -962,6 +1044,20 @@ "pend": "~1.2.0" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -976,10 +1072,13 @@ "dev": true }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gauge": { "version": "2.7.4", @@ -1050,14 +1149,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1090,16 +1194,16 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dev": true, "dependencies": { - "function-bind": "^1.1.1" + "get-intrinsic": "^1.1.3" }, - "engines": { - "node": ">= 0.4.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { @@ -1111,10 +1215,34 @@ "node": ">=4" } }, - "node_modules/has-symbols": { + "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true, "engines": { "node": ">= 0.4" @@ -1130,6 +1258,18 @@ "dev": true, "optional": true }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hosted-git-info": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", @@ -1312,6 +1452,27 @@ "node": ">=4" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -1430,9 +1591,9 @@ } }, "node_modules/object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1542,12 +1703,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -1660,9 +1821,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -1680,15 +1841,36 @@ "dev": true, "optional": true }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1890,9 +2072,9 @@ } }, "node_modules/typed-rest-client": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", - "integrity": "sha512-xcQpTEAJw2DP7GqVNECh4dD+riS+C1qndXLfBCJ3xk0kqprtGN491P5KlmrDbKdtuW8NEcP/5ChxiJI3S9WYTA==", + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, "dependencies": { "qs": "^6.9.1", @@ -1901,9 +2083,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -1920,9 +2102,9 @@ "dev": true }, "node_modules/underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, "node_modules/url-join": { @@ -2057,163 +2239,163 @@ }, "dependencies": { "@esbuild/aix-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", - "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", - "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", - "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", - "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", - "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", - "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", - "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", - "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", - "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", - "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", - "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", - "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", - "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", - "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", - "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", - "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", - "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", - "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", - "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", - "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", - "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", - "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", - "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "dev": true, "optional": true }, @@ -2251,15 +2433,17 @@ "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==" }, "@vscode/vsce": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.22.0.tgz", - "integrity": "sha512-8df4uJiM3C6GZ2Sx/KilSKVxsetrTBBIUb3c0W4B1EWHcddioVs5mkyDKtMNP0khP/xBILVSzlXxhV+nm2rC9A==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.25.0.tgz", + "integrity": "sha512-VXMCGUaP6wKBadA7vFQdsksxkBAMoh4ecZgXBwauZMASAgnwYesHyLnqIyWYeRwjy2uEpitHvz/1w5ENnR30pg==", "dev": true, "requires": { - "azure-devops-node-api": "^11.0.1", + "azure-devops-node-api": "^12.5.0", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", "commander": "^6.2.1", + "form-data": "^4.0.0", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", @@ -2319,10 +2503,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "azure-devops-node-api": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", - "integrity": "sha512-YMdjAw9l5p/6leiyIloxj3k7VIvYThKjvqgiQn88r3nhT93ENwsoDS3A83CyJ4uTWzCZ5f5jCi6c27rTU5Pz+A==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, "requires": { "tunnel": "0.0.6", @@ -2401,13 +2591,16 @@ "dev": true }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "chalk": { @@ -2456,6 +2649,12 @@ "dev": true, "optional": true }, + "cockatiel": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.2.tgz", + "integrity": "sha512-5yARKww0dWyWg2/3xZeXgoxjHLwpVqFptj9Zy7qioJ6+/L0ARM184sgMUrQDjxw7ePJWlGhV998mKhzrxT0/Kg==", + "dev": true + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -2478,6 +2677,15 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -2540,6 +2748,23 @@ "dev": true, "optional": true }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -2607,35 +2832,50 @@ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, "esbuild": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", - "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.19.11", - "@esbuild/android-arm": "0.19.11", - "@esbuild/android-arm64": "0.19.11", - "@esbuild/android-x64": "0.19.11", - "@esbuild/darwin-arm64": "0.19.11", - "@esbuild/darwin-x64": "0.19.11", - "@esbuild/freebsd-arm64": "0.19.11", - "@esbuild/freebsd-x64": "0.19.11", - "@esbuild/linux-arm": "0.19.11", - "@esbuild/linux-arm64": "0.19.11", - "@esbuild/linux-ia32": "0.19.11", - "@esbuild/linux-loong64": "0.19.11", - "@esbuild/linux-mips64el": "0.19.11", - "@esbuild/linux-ppc64": "0.19.11", - "@esbuild/linux-riscv64": "0.19.11", - "@esbuild/linux-s390x": "0.19.11", - "@esbuild/linux-x64": "0.19.11", - "@esbuild/netbsd-x64": "0.19.11", - "@esbuild/openbsd-x64": "0.19.11", - "@esbuild/sunos-x64": "0.19.11", - "@esbuild/win32-arm64": "0.19.11", - "@esbuild/win32-ia32": "0.19.11", - "@esbuild/win32-x64": "0.19.11" + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "escape-string-regexp": { @@ -2660,6 +2900,17 @@ "pend": "~1.2.0" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -2674,9 +2925,9 @@ "dev": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true }, "gauge": { @@ -2738,14 +2989,16 @@ } }, "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "github-from-package": { @@ -2769,13 +3022,13 @@ "path-is-absolute": "^1.0.0" } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dev": true, "requires": { - "function-bind": "^1.1.1" + "get-intrinsic": "^1.1.3" } }, "has-flag": { @@ -2784,10 +3037,25 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "has-symbols": { + "has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true }, "has-unicode": { @@ -2797,6 +3065,15 @@ "dev": true, "optional": true }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, "hosted-git-info": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", @@ -2935,6 +3212,21 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, "mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -3032,9 +3324,9 @@ "optional": true }, "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true }, "once": { @@ -3131,12 +3423,12 @@ } }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", "dev": true, "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "rc": { @@ -3218,9 +3510,9 @@ "dev": true }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "requires": { "lru-cache": "^6.0.0" } @@ -3232,15 +3524,30 @@ "dev": true, "optional": true }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { @@ -3391,9 +3698,9 @@ } }, "typed-rest-client": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", - "integrity": "sha512-xcQpTEAJw2DP7GqVNECh4dD+riS+C1qndXLfBCJ3xk0kqprtGN491P5KlmrDbKdtuW8NEcP/5ChxiJI3S9WYTA==", + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, "requires": { "qs": "^6.9.1", @@ -3402,9 +3709,9 @@ } }, "typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true }, "uc.micro": { @@ -3414,9 +3721,9 @@ "dev": true }, "underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, "url-join": { diff --git a/code/package.json b/code/package.json index f28bd6e0a..4600438cb 100644 --- a/code/package.json +++ b/code/package.json @@ -19,7 +19,8 @@ "scripts": { "compile": "node esbuild.mjs", "watch": "tsc -p . --watch", - "deploy": "vsce publish --pre-release -i *.vsix --baseImagesUrl https://github.com/swyddfa/esbonio/raw/release/code/", + "deploy-vsce": "vsce publish --pre-release -i *.vsix --baseImagesUrl https://github.com/swyddfa/esbonio/raw/release/code/", + "deploy-ovsx": "ovsx publish --pre-release -i *.vsix --baseImagesUrl https://github.com/swyddfa/esbonio/raw/release/code/", "package": "vsce package --pre-release --baseImagesUrl https://github.com/swyddfa/esbonio/raw/release/code/", "vscode:prepublish": "npm run compile" }, @@ -28,7 +29,7 @@ "ms-python.python" ], "dependencies": { - "semver": "^7.5.4", + "semver": "^7.6.0", "@vscode/python-extension": "^1.0.5", "vscode-languageclient": "^9.0.1" }, @@ -36,9 +37,9 @@ "@types/glob": "^8.1.0", "@types/node": "^18", "@types/vscode": "1.78.0", - "@vscode/vsce": "^2.22.0", - "esbuild": "^0.19.11", - "typescript": "^5.3.3" + "@vscode/vsce": "^2.25.0", + "esbuild": "^0.20.2", + "typescript": "^5.4.5" }, "engines": { "vscode": "^1.82.0" @@ -88,11 +89,28 @@ "default": true, "description": "Enable/Disable the language server" }, - "esbonio.server.enabledInPyFiles": { + "esbonio.server.documentSelector": { "scope": "window", - "type": "boolean", - "default": true, - "description": "Enable/Disable the language server in Python files." + "type": "array", + "items": { + "type": "object", + "properties": { + "scheme": { + "type": "string", + "description": "The URI scheme that this selector applies to" + }, + "language": { + "type": "string", + "description": "The language id this selector will select" + }, + "pattern": { + "type": "string", + "description": "Only select uris that match the given pattern" + } + } + }, + "default": [], + "description": "Override the extension's default document selector" }, "esbonio.server.startupModule": { "scope": "window", @@ -118,26 +136,6 @@ }, "description": "A list of additional modules to include in the server's configuration" }, - "esbonio.server.logLevel": { - "scope": "window", - "type": "string", - "default": null, - "enum": [ - "debug", - "info", - "error" - ], - "description": "The level of log message to show in the log" - }, - "esbonio.server.logFilter": { - "scope": "window", - "type": "array", - "default": null, - "items": { - "type": "string" - }, - "description": "If set, only messages from the named loggers will be shown" - }, "esbonio.server.pythonPath": { "scope": "window", "type": "string", @@ -229,6 +227,86 @@ } } }, + { + "title": "Logging", + "properties": { + "esbonio.logging.level": { + "scope": "window", + "type": "string", + "default": "error", + "enum": [ + "critical", + "fatal", + "error", + "warning", + "info", + "debug" + ], + "description": "The default level of log message to show in the log" + }, + "esbonio.logging.format": { + "scope": "window", + "type": "string", + "default": null, + "description": "The default format string to apply to log messages" + }, + "esbonio.logging.filepath": { + "scope": "window", + "type": "string", + "default": null, + "description": "If set, record log messages in the given filepath (path is relative to the server's working directory)." + }, + "esbonio.logging.stderr": { + "scope": "window", + "type": "boolean", + "default": true, + "description": "If set, print log messages to stderr" + }, + "esbonio.logging.window": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "If set, send log messages as window/logMessage notifications" + }, + "esbonio.logging.config": { + "scope": "window", + "type": "object", + "patternProperties": { + ".*": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "critical", + "fatal", + "error", + "warning", + "info", + "debug" + ], + "description": "The level of log message to show in the log for this logger" + }, + "filepath": { + "type": "string", + "description": "If set, log messages from this logger to the given file (path is relative to server working directory)." + }, + "stderr": { + "type": "boolean", + "description": "If set, print messages from this logger to stderr." + }, + "window": { + "type": "boolean", + "description": "If set, send messages from this logger as window/logMessage notifications." + } + } + } + }, + "default": {}, + "description": "Low level logging configuration settings to apply to individual loggers" + } + } + }, { "title": "Developer Options", "properties": { @@ -238,6 +316,22 @@ "default": false, "description": "Enable lsp-devtools integration for the language server" }, + "esbonio.trace.server": { + "scope": "window", + "type": "string", + "default": "off", + "enum": [ + "off", + "messages", + "verbose" + ], + "description": "Controls if LSP messages sent to/from the server should be logged.", + "enumDescriptions": [ + "do not log any lsp messages", + "log all lsp messages sent to/from the server", + "log all lsp messages sent to/from the server, including their contents" + ] + }, "esbonio.server.debug": { "scope": "window", "type": "boolean", diff --git a/code/requirements.txt b/code/requirements.txt index 61dfe0c28..fcd9a23a3 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -4,9 +4,9 @@ # # pip-compile --generate-hashes ./requirements.in # -aiosqlite==0.19.0 \ - --hash=sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d \ - --hash=sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96 +aiosqlite==0.20.0 \ + --hash=sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6 \ + --hash=sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7 # via -r requirements.in attrs==23.1.0 \ --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ @@ -17,7 +17,9 @@ attrs==23.1.0 \ cattrs==23.2.2 \ --hash=sha256:66064e2060ea207c5a48d065ab1910c10bb8108c28f3df8d1a7b1aa6b19d191b \ --hash=sha256:b790b1c2be1ce042611e33f740e343c2593918bbf3c1cc88cdddac4defc09655 - # via lsprotocol + # via + # lsprotocol + # pygls docutils==0.20.1 \ --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \ --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b @@ -26,17 +28,17 @@ exceptiongroup==1.2.0 \ --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \ --hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68 # via cattrs -lsprotocol==2023.0.0 \ - --hash=sha256:c9d92e12a3f4ed9317d3068226592860aab5357d93cf5b2451dc244eee8f35f2 \ - --hash=sha256:e85fc87ee26c816adca9eb497bb3db1a7c79c477a11563626e712eaccf926a05 +lsprotocol==2023.0.1 \ + --hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \ + --hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d # via pygls -platformdirs==4.1.0 \ - --hash=sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380 \ - --hash=sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420 +platformdirs==4.2.0 \ + --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ + --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 # via -r requirements.in -pygls==1.2.1 \ - --hash=sha256:04f9b9c115b622dcc346fb390289066565343d60245a424eca77cb429b911ed8 \ - --hash=sha256:7dcfcf12b6f15beb606afa46de2ed348b65a279c340ef2242a9a35c22eeafe94 +pygls==1.3.1 \ + --hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \ + --hash=sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e # via -r requirements.in tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ @@ -45,7 +47,9 @@ tomli==2.0.1 \ typing-extensions==4.8.0 \ --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef - # via cattrs + # via + # aiosqlite + # cattrs websockets==12.0 \ --hash=sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b \ --hash=sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6 \ diff --git a/code/src/common/constants.ts b/code/src/common/constants.ts index abcc3b29f..395c36d41 100644 --- a/code/src/common/constants.ts +++ b/code/src/common/constants.ts @@ -1,5 +1,11 @@ export namespace Server { export const REQUIRED_PYTHON = "3.8.0" + + export const DEFAULT_SELECTOR = [ + { scheme: 'file', language: 'restructuredtext' }, + { scheme: 'file', language: 'markdown' }, + // { scheme: 'file', language: 'python' } + ] } export namespace Commands { @@ -16,6 +22,8 @@ export namespace Commands { export namespace Events { export const SERVER_START = "server/start" export const SERVER_STOP = "server/stop" + + export const PYTHON_ENV_CHANGE = "python/envChange" } /** @@ -25,5 +33,8 @@ export namespace Notifications { export const SCROLL_EDITOR = "editor/scroll" export const VIEW_SCROLL = "view/scroll" + export const SPHINX_CLIENT_CREATED = "sphinx/clientCreated" + export const SPHINX_CLIENT_ERRORED = "sphinx/clientErrored" + export const SPHINX_CLIENT_DESTROYED = "sphinx/clientDestroyed" export const SPHINX_APP_CREATED = "sphinx/appCreated" } diff --git a/code/src/node/client.ts b/code/src/node/client.ts index 461a6f487..b072ace50 100644 --- a/code/src/node/client.ts +++ b/code/src/node/client.ts @@ -1,5 +1,6 @@ import { execSync } from "child_process"; import * as vscode from 'vscode'; +import { ActiveEnvironmentPathChangeEvent } from '@vscode/python-extension'; import { join } from "path"; import { CancellationToken, @@ -8,21 +9,84 @@ import { LanguageClientOptions, ResponseError, ServerOptions, - State + State, + TextDocumentFilter } from "vscode-languageclient/node"; import { OutputChannelLogger } from "../common/log"; import { PythonManager } from "./python"; -import { Commands, Events, Notifications } from '../common/constants'; +import { Commands, Events, Notifications, Server } from '../common/constants'; -export interface SphinxInfo { +export interface SphinxClientConfig { + + /** + * The python command used to launch the client + */ + pythonCommand: string[] + + /** + * The sphinx-build command in use + */ + buildCommand: string[] /** - * A unique id used to refer to this Sphinx application instance. + * The working directory of the client + */ + cwd: string + +} + +export interface ClientCreatedNotification { + /** + * A unique id for this client */ id: string + /** + * The configuration scope at which the client was created + */ + scope: string + + /** + * The final configuration + */ + config: SphinxClientConfig + +} + +/** + * The payload of a ``sphinx/clientErrored`` notification + */ +export interface ClientErroredNotification { + + /** + * A unique id for the client + */ + id: string + + /** + * Short description of the error. + */ + error: string + + /** + * Detailed description of the error. + */ + detail: string +} + + +export interface ClientDestroyedNotification { + /** + * A unique id for this client + */ + id: string +} + +export interface SphinxInfo { + + /** * Sphinx's version number */ @@ -51,6 +115,18 @@ export interface SphinxInfo { src_dir: string } +export interface AppCreatedNotification { + + /** + * A unique id for this client + */ + id: string + + /** + * Details about the created application. + */ + application: SphinxInfo +} export class EsbonioClient { @@ -81,6 +157,11 @@ export class EsbonioClient { } }) ) + + // React to environment changes in the Python extension + python.addHandler(Events.PYTHON_ENV_CHANGE, (_event: ActiveEnvironmentPathChangeEvent) => { + this.client?.sendNotification("workspace/didChangeConfiguration", { settings: null }) + }) } public addHandler(event: string, handler: any) { @@ -237,6 +318,9 @@ export class EsbonioClient { let methods = [ Notifications.SCROLL_EDITOR, Notifications.SPHINX_APP_CREATED, + Notifications.SPHINX_CLIENT_CREATED, + Notifications.SPHINX_CLIENT_ERRORED, + Notifications.SPHINX_CLIENT_DESTROYED, ] for (let method of methods) { @@ -252,15 +336,11 @@ export class EsbonioClient { */ private getLanguageClientOptions(config: vscode.WorkspaceConfiguration): LanguageClientOptions { - let documentSelector = [ - { scheme: 'file', language: 'restructuredtext' }, - { scheme: 'file', language: 'markdown' }, - ] - if (config.get('server.enabledInPyFiles')) { - documentSelector.push( - { scheme: 'file', language: 'python' } - ) + + let documentSelector = config.get("server.documentSelector") + if (!documentSelector || documentSelector.length === 0) { + documentSelector = Server.DEFAULT_SELECTOR } let clientOptions: LanguageClientOptions = { diff --git a/code/src/node/extension.ts b/code/src/node/extension.ts index e22905ac3..99437a18d 100644 --- a/code/src/node/extension.ts +++ b/code/src/node/extension.ts @@ -1,5 +1,5 @@ -// PYTHONPATH="$(pwd)/bundled/libs" python -S -c "import sys;print('\n'.join(sys.path))" import * as vscode from 'vscode' +import { PythonExtension } from '@vscode/python-extension'; import { OutputChannelLogger } from '../common/log' import { PythonManager } from './python' @@ -12,12 +12,13 @@ let logger: OutputChannelLogger export async function activate(context: vscode.ExtensionContext) { let channel = vscode.window.createOutputChannel("Esbonio", "esbonio-log-output") - let logLevel = vscode.workspace.getConfiguration('esbonio').get('server.logLevel') + let logLevel = vscode.workspace.getConfiguration('esbonio').get('logging.level') logger = new OutputChannelLogger(channel, logLevel) - let python = new PythonManager(logger) - esbonio = new EsbonioClient(logger, python, context, channel) + let python = await getPythonExtension() + let pythonManager = new PythonManager(python, logger, context) + esbonio = new EsbonioClient(logger, pythonManager, context, channel) let previewManager = new PreviewManager(logger, context, esbonio) let statusManager = new StatusManager(logger, context, esbonio) @@ -28,6 +29,18 @@ export async function activate(context: vscode.ExtensionContext) { } } +/** + * Return the python extension's API, if available. + */ +async function getPythonExtension(): Promise { + try { + return await PythonExtension.api() + } catch (err) { + logger.error(`Unable to load python extension: ${err}`) + return undefined + } +} + export function deactivate(): Thenable | undefined { if (!esbonio) { return undefined diff --git a/code/src/node/preview.ts b/code/src/node/preview.ts index 1f9f7fb50..fb5c87447 100644 --- a/code/src/node/preview.ts +++ b/code/src/node/preview.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import { OutputChannelLogger } from '../common/log' import { EsbonioClient } from './client' -import { Commands, Events, Notifications, Server } from '../common/constants' +import { Commands, Events, Notifications } from '../common/constants' interface PreviewFileParams { uri: string diff --git a/code/src/node/python.ts b/code/src/node/python.ts index e07c558ea..30a9bee6e 100644 --- a/code/src/node/python.ts +++ b/code/src/node/python.ts @@ -2,13 +2,31 @@ import * as vscode from 'vscode' import { PythonExtension } from '@vscode/python-extension'; import { OutputChannelLogger } from "../common/log"; +import { Events } from '../common/constants'; export class PythonManager { - constructor(private logger: OutputChannelLogger) { } + private handlers: Map + + constructor( + private python: PythonExtension | undefined, + private logger: OutputChannelLogger, + context: vscode.ExtensionContext + ) { + this.handlers = new Map() + + if (python) { + context.subscriptions.push( + python.environments.onDidChangeActiveEnvironmentPath((event) => { + logger.debug(`Changed active Python env: ${JSON.stringify(event, undefined, 2)}`) + this.callHandlers(Events.PYTHON_ENV_CHANGE, event) + }) + ) + } + } async getCmd(scopeUri?: vscode.Uri): Promise { - let userPython = vscode.workspace.getConfiguration("esbonio").get("server.pythonPath") + let userPython = vscode.workspace.getConfiguration("esbonio", scopeUri).get("server.pythonPath") if (userPython) { // Support for ${workspaceRoot}/... @@ -28,15 +46,14 @@ export class PythonManager { return [userPython] } - let python = await this.getPythonExtension() - if (!python) { + if (!this.python) { return } - let activeEnvPath = python.environments.getActiveEnvironmentPath(scopeUri) + let activeEnvPath = this.python.environments.getActiveEnvironmentPath(scopeUri) this.logger.debug(`Using environment ${activeEnvPath.id}: ${activeEnvPath.path}`) - let activeEnv = await python.environments.resolveEnvironment(activeEnvPath) + let activeEnv = await this.python.environments.resolveEnvironment(activeEnvPath) if (!activeEnv) { this.logger.debug("Unable to resolve environment") return @@ -52,33 +69,38 @@ export class PythonManager { } async getDebugerCommand(): Promise { - let python = await this.getPythonExtension() - if (!python) { + if (!this.python) { return [] } - return await python.debug.getRemoteLauncherCommand('localhost', 5678, true) + return await this.python.debug.getRemoteLauncherCommand('localhost', 5678, true) } async getDebugerPath(): Promise { - let python = await this.getPythonExtension() - if (!python) { + if (!this.python) { return '' } - let path = await python.debug.getDebuggerPackagePath() + let path = await this.python.debug.getDebuggerPackagePath() return path || '' } - /** - * Ensures that if the Python extension is available - */ - private async getPythonExtension(): Promise { - try { - return await PythonExtension.api() - } catch (err) { - this.logger.error(`Unable to load python extension: ${err}`) - return undefined + + public addHandler(event: string, handler: any) { + if (this.handlers.has(event)) { + this.handlers.get(event)?.push(handler) + } else { + this.handlers.set(event, [handler]) } } + private callHandlers(method: string, params: any) { + this.handlers.get(method)?.forEach(handler => { + try { + handler(params) + } catch (err) { + this.logger.error(`Error in '${method}' notification handler: ${err}`) + } + }) + } + } diff --git a/code/src/node/status.ts b/code/src/node/status.ts index 72483fc17..2bad5672f 100644 --- a/code/src/node/status.ts +++ b/code/src/node/status.ts @@ -1,9 +1,16 @@ -import * as vscode from 'vscode'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { TextDocumentFilter } from 'vscode-languageclient'; +import { Events, Notifications, Server } from '../common/constants'; import { OutputChannelLogger } from "../common/log"; -import { EsbonioClient, SphinxInfo } from './client'; -import { Events, Notifications } from '../common/constants'; +import { + AppCreatedNotification, + ClientCreatedNotification, + ClientDestroyedNotification, + ClientErroredNotification, + EsbonioClient +} from './client'; interface StatusItemFields { busy?: boolean @@ -31,35 +38,107 @@ export class StatusManager { client.addHandler( Notifications.SPHINX_APP_CREATED, - (params: SphinxInfo) => { this.createApp(params) } + (params: AppCreatedNotification) => { this.appCreated(params) } + ) + + client.addHandler( + Notifications.SPHINX_CLIENT_CREATED, + (params: ClientCreatedNotification) => { this.clientCreated(params) } + ) + + client.addHandler( + Notifications.SPHINX_CLIENT_ERRORED, + (params: ClientErroredNotification) => { this.clientErrored(params) } + ) + + client.addHandler( + Notifications.SPHINX_CLIENT_DESTROYED, + (params: ClientDestroyedNotification) => { this.clientDestroyed(params) } ) } - private createApp(info: SphinxInfo) { + private clientCreated(params: ClientCreatedNotification) { + this.logger.debug(`${Notifications.SPHINX_CLIENT_CREATED}: ${JSON.stringify(params, undefined, 2)}`) + let sphinxConfig = params.config - let confUri = vscode.Uri.file(info.conf_dir) - let workspaceFolder = vscode.workspace.getWorkspaceFolder(confUri) - if (!workspaceFolder) { - this.logger.error(`Unable to find workspace containing: ${info.conf_dir}`) - return + let config = vscode.workspace.getConfiguration("esbonio.server") + let documentSelector = config.get("documentSelector") + if (!documentSelector || documentSelector.length === 0) { + documentSelector = Server.DEFAULT_SELECTOR } let selector: vscode.DocumentFilter[] = [] + let defaultPattern = path.join(sphinxConfig.cwd, "**", "*") + for (let docSelector of documentSelector) { + selector.push({ + scheme: docSelector.scheme, + language: docSelector.language, + pattern: docSelector.pattern || defaultPattern + }) + } - let confPattern = uriToPattern(confUri) - selector.push({ language: "python", pattern: confPattern }) + this.setStatusItem( + params.id, + "sphinx", + "Sphinx[starting]", + { + selector: selector, + busy: true, + detail: sphinxConfig.buildCommand.join(" "), + severity: vscode.LanguageStatusSeverity.Information + } + ) + this.setStatusItem( + params.id, + "python", + "Python", + { + selector: selector, + detail: sphinxConfig.pythonCommand.join(" "), + command: { title: "Change Interpreter", command: "python.setInterpreter" }, + severity: vscode.LanguageStatusSeverity.Information + } + ) + } - let srcUri = vscode.Uri.file(info.src_dir) - let srcPattern = uriToPattern(srcUri); - selector.push({ language: 'restructuredtext', pattern: srcPattern }) + private clientErrored(params: ClientErroredNotification) { + this.logger.debug(`${Notifications.SPHINX_CLIENT_ERRORED}: ${JSON.stringify(params, undefined, 2)}`) + + this.setStatusItem( + params.id, + "sphinx", + "Sphinx[failed]", + { + busy: false, + detail: params.error, + severity: vscode.LanguageStatusSeverity.Error + } + ) + } - let itemId = `${workspaceFolder.uri}` - let buildUri = vscode.Uri.file(info.build_dir) - this.setStatusItem(itemId, "sphinx", `Sphinx v${info.version}`, { selector: selector }) - this.setStatusItem(itemId, "builder", `Builder - ${info.builder_name}`, { selector: selector }) - this.setStatusItem(itemId, "srcdir", `Source - ${renderPath(workspaceFolder, srcUri)}`, { selector: selector }) - this.setStatusItem(itemId, "confdir", `Config - ${renderPath(workspaceFolder, confUri)}`, { selector: selector }) - this.setStatusItem(itemId, "builddir", `Build - ${renderPath(workspaceFolder, buildUri)}`, { selector: selector }) + private clientDestroyed(params: ClientDestroyedNotification) { + this.logger.debug(`${Notifications.SPHINX_CLIENT_DESTROYED}: ${JSON.stringify(params, undefined, 2)}`) + + for (let [key, item] of this.statusItems.entries()) { + if (key.startsWith(params.id)) { + item.dispose() + this.statusItems.delete(key) + } + } + } + + private appCreated(params: AppCreatedNotification) { + this.logger.debug(`${Notifications.SPHINX_APP_CREATED}: ${JSON.stringify(params, undefined, 2)}`) + let sphinx = params.application + + this.setStatusItem( + params.id, + "sphinx", + `Sphinx[${sphinx.builder_name}] v${sphinx.version}`, + { + busy: false, + } + ) } private serverStop(_params: any) { @@ -70,12 +149,12 @@ export class StatusManager { } private setStatusItem( - sphinxId: string, + id: string, name: string, value: string, params?: StatusItemFields, ) { - let key = `${sphinxId}-${name.toLocaleLowerCase().replace(' ', '-')}` + let key = `${id}-${name.toLocaleLowerCase().replace(' ', '-')}` let statusItem = this.statusItems.get(key) if (!statusItem) { @@ -108,24 +187,3 @@ export class StatusManager { } } } - -function renderPath(workspace: vscode.WorkspaceFolder, uri: vscode.Uri): string { - let workspacePath = workspace.uri.fsPath - let uriPath = uri.fsPath - - let result = uriPath - - if (uriPath.startsWith(workspacePath)) { - result = path.join('.', result.replace(workspacePath, '')) - } - - if (result.length > 50) { - result = '...' + result.slice(result.length - 47) - } - - return result -} - -function uriToPattern(uri: vscode.Uri) { - return path.join(uri.fsPath, "**", "*").replace(/\\/g, '/'); -} diff --git a/docs/conf.py b/docs/conf.py index 991760c73..c89a8876e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,27 +10,20 @@ # import os import sys -from typing import List sys.path.insert(0, os.path.abspath("../lib/esbonio")) sys.path.insert(0, os.path.abspath("./ext")) from docutils.parsers.rst import nodes -from lsprotocol.types import METHOD_TO_TYPES -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionItemKind from sphinx.application import Sphinx -import esbonio.lsp -from esbonio.lsp.roles import Roles -from esbonio.lsp.roles import TargetCompletion -from esbonio.lsp.rst import CompletionContext +import esbonio.server # -- Project information ----------------------------------------------------- project = "Esbonio" -copyright = "2023" +copyright = "2024" author = "the Esbonio project" -release = esbonio.lsp.__version__ +release = esbonio.server.__version__ DEV_BUILD = os.getenv("BUILDDIR", None) == "latest" BRANCH = "develop" if DEV_BUILD else "release" @@ -62,8 +55,6 @@ autodoc_typehints = "description" autodoc_typehints_description_target = "documented" -autodoc_pydantic_model_show_json = True - intersphinx_mapping = { "ipython": ("https://ipython.readthedocs.io/en/stable/", None), "python": ("https://docs.python.org/3/", None), @@ -90,37 +81,12 @@ "source_repository": "https://github.com/swyddfa/esbonio/", "source_branch": BRANCH, "source_directory": "docs/", + "announcement": ( + "This is the documentation for the in-development 1.0 release of the language server. " + 'Click here to view the documentation for the current stable version' + ), } -if DEV_BUILD: - html_theme_options["announcement"] = ( - "This is the unstable version of the documentation, features may change or be removed without warning. " - 'Click here to view the released version' - ) - - -class LspMethod(TargetCompletion): - """Provides completion suggestions for the custom ``:lsp:`` role.""" - - def __init__(self) -> None: - super().__init__() - self._index_methods() - - def _index_methods(self): - self.items = [] - - for method in METHOD_TO_TYPES.keys(): - item = CompletionItem(label=method, kind=CompletionItemKind.Constant) - self.items.append(item) - - def complete_targets( - self, context: CompletionContext, name: str, domain: str - ) -> List[CompletionItem]: - if name == "lsp": - return self.items - - return [] - def lsp_role(name, rawtext, text, lineno, inliner, options={}, content=[]): """Link to sections within the lsp specification.""" @@ -167,7 +133,3 @@ def setup(app: Sphinx): objname="IPython magic", indextemplate="pair: %s; IPython magic", ) - - -def esbonio_setup(roles: Roles): - roles.add_target_completion_provider(LspMethod()) diff --git a/docs/ext/collection_items.py b/docs/ext/collection_items.py index 27bd62d8a..9e71b0574 100644 --- a/docs/ext/collection_items.py +++ b/docs/ext/collection_items.py @@ -19,6 +19,7 @@ ... """ + import string import uuid @@ -27,12 +28,10 @@ from sphinx.application import Sphinx -class collection(nodes.General, nodes.Element): - ... +class collection(nodes.General, nodes.Element): ... -class collection_item(nodes.General, nodes.Element): - ... +class collection_item(nodes.General, nodes.Element): ... STYLE_TEMPLATE = string.Template( diff --git a/docs/index.rst b/docs/index.rst index 2d3f75796..ae1fc6f3b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,85 +3,42 @@ Esbonio .. rubric:: esbonio -- (v.) to explain -Esbonio aims to make it easier to work with `reStructuredText`_ tools such as -`Sphinx`_ by providing a `Language Server`_ to enhance your editing experience. +Esbonio is a `Language Server`_ for `Sphinx`_ documentation projects. -Language Server ---------------- +Esbonio aids the writing process by resolving references, providing completion suggestions and highlighting errors. +It ensures your local build is always up to date, allowing you to preview your changes in (almost!) real-time. +The server itself can even be extended to better suit the needs of your project. -Here is a quick summary of the features implemented by the language server. +The primary goal of Esbonio is to reduce the friction that comes from trying to remember the specifics of a markup language, so that you can focus on your content and not your tooling. -.. collection:: features +.. grid:: 2 + :gutter: 2 - .. collection-item:: Completion + .. grid-item-card:: Getting Started + :text-align: center + :link: lsp-getting-started + :link-type: ref - The language server implements :lsp:`textDocument/completion` and can - offer suggestions in a variety of contexts. + Using Esbonio for the first time within VSCode. - .. figure:: ../resources/images/completion-demo.gif - :align: center - :target: /_images/completion-demo.gif + .. grid-item-card:: How-To Guides + :text-align: center - .. collection-item:: Definition + Step-by-step guides on integrating Esbonio with other text editors. - The language server implements :lsp:`textDocument/definition` to provide the - location of items referenced by certain roles. Currently only the ``:ref:`` - and ``:doc:`` roles are supported. + .. grid-item-card:: Reference + :text-align: center + :link: lsp-reference + :link-type: ref - .. figure:: ../resources/images/definition-demo.gif - :align: center - :target: /_images/definition-demo.gif + Configuration options, API documentation, architecture diagrams and more. - .. collection-item:: Diagnostics - - The language server implements :lsp:`textDocument/publishDiagnostics` to - report errors/warnings enountered during a build. - - .. figure:: ../resources/images/diagnostic-sphinx-errors-demo.png - :align: center - :target: /_images/diagnostic-sphinx-errors-demo.png - - .. collection-item:: Document Links - - The language server implements :lsp:`textDocument/documentLink` to make references to other files "Ctrl + Clickable" - - .. figure:: ../resources/images/document-links-demo.png - :align: center - :target: /_images/document-links-demo.png - - .. collection-item:: Document Symbols - - The language server implements :lsp:`textDocument/documentSymbol` which - powers features like the "Outline" view in VSCode. - - .. figure:: ../resources/images/document-symbols-demo.png - :align: center - :target: /_images/document-symbols-demo.png - - .. collection-item:: Hover - - The language server implements :lsp:`textDocument/hover` to provide easy access to documentation for roles and directives. - - .. figure:: ../resources/images/hover-demo.png - :align: center - :target: /_images/hover-demo.png - - .. collection-item:: Implementation - - The language server implements :lsp:`textDocument/implementation` so you can easily find the implementation of a given role or directive. - - .. figure:: ../resources/images/implementation-demo.gif - :align: center - :target: /_images/implementation-demo.gif - -- See the :ref:`lsp_getting_started` guide for details on how to get up and - running. - -- For further details on more advanced use cases, see the :ref:`lsp-advanced` section. - -- Interested in adding support for your own Sphinx extensions? - See the section on :ref:`lsp-extending` for more information. + .. grid-item-card:: Extending + :text-align: center + :link: lsp-extending + :link-type: ref + Documentation on extending the language server .. toctree:: :glob: @@ -90,7 +47,6 @@ Here is a quick summary of the features implemented by the language server. :maxdepth: 2 lsp/getting-started - lsp/advanced-usage lsp/extending lsp/howto lsp/reference diff --git a/docs/lsp/_architecture.rst b/docs/lsp/_architecture.rst deleted file mode 100644 index cb957f26e..000000000 --- a/docs/lsp/_architecture.rst +++ /dev/null @@ -1,230 +0,0 @@ -.. raw:: html - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - Language Feature #1 - Language Feature #2 - Language Server - Language Client - LSP Protocol - - - Engine - - ..... - Language Feature #N - - - - - -

A rough sketch of how the language server(s) in Esbonio are architected.

-
diff --git a/docs/lsp/advanced-usage.rst b/docs/lsp/advanced-usage.rst deleted file mode 100644 index 378b7dbb1..000000000 --- a/docs/lsp/advanced-usage.rst +++ /dev/null @@ -1,161 +0,0 @@ -.. _lsp-advanced: - -Advanced Usage -============== - -The :doc:`/lsp/getting-started` guide should contain all you need to get up and running with your -editor of choice. However there may come a time when you will want to enable/disable certain -functionality or use a different server entirely. - -**Wait.. there are different servers?** - -Yes there are! - -Due to the extensible nature of Sphinx and reStructuredText, the ``esbonio`` python package -is actually a framework for building reStructuredText language servers. It just so happens -that it also comes with a default implementation that works well for Sphinx projects (see -the section on :doc:`/lsp/extending` if you want to know more) - -However, all that we need to know for the moment is the concept of startup modules. - -.. _lsp-startup-mods: - -Startup Modules ---------------- - -A startup module is any python module (or script) that results in a running language server. -The following startup modules are included with the ``esbonio`` python package. - -.. startmod:: esbonio - - The default startup module you are probably already familiar with. - It is in fact just an alias for the :startmod:`esbonio.lsp.sphinx` startup module. - - .. .. cli-help:: esbonio.__main__ - -.. startmod:: esbonio.lsp.rst - - A "vanilla" reStructuedText language server for use with docutils projects. - - .. .. cli-help:: esbonio.lsp.rst - -.. startmod:: esbonio.lsp.sphinx - - A language server tailored for use with Sphinx projects. - - .. .. cli-help:: esbonio.lsp.sphinx - -.. _lsp-extension-modules: - -Extension Modules ------------------ - -Inspired by the way Sphinx extensions work, functionality is added to a language server through a list of python modules with each module contributing some features. - -Below is the list of modules loaded by default for each of the provided servers. - -.. relevant-to:: Startup Module - - esbonio - .. literalinclude:: ../../lib/esbonio/esbonio/lsp/sphinx/__init__.py - :language: python - :start-at: DEFAULT_MODULES - :end-at: ] - - esbonio.lsp.rst - .. literalinclude:: ../../lib/esbonio/esbonio/lsp/rst/__init__.py - :language: python - :start-at: DEFAULT_MODULES - :end-at: ] - - esbonio.lsp.sphinx - .. literalinclude:: ../../lib/esbonio/esbonio/lsp/sphinx/__init__.py - :language: python - :start-at: DEFAULT_MODULES - :end-at: ] - -In addition to the modules enabled by default, the following modules are provided and can be -enabled if you wish. - -.. extmod:: esbonio.lsp.spelling - - **Experimental** - - Basic spell checking, with errors reported as diagnostics and corrections suggested as code actions. - Currently only available for English and can be confused by reStructuredText syntax. - -Commands --------- - -The bundled language servers offer some commands that can be invoked from a language client using -a :lsp:`workspace/executeCommand` request. - - -.. command:: esbonio.server.build - - .. relevant-to:: Startup Module - - esbonio - .. include:: ./advanced/_esbonio.lsp.sphinx_build_command.rst - - esbonio.lsp.rst - Currently a placeholder. - - esbonio.lsp.sphinx - .. include:: ./advanced/_esbonio.lsp.sphinx_build_command.rst - -.. command:: esbonio.server.configuration - - .. relevant-to:: Startup Module - - esbonio - .. include:: ./advanced/_esbonio.lsp.sphinx_configuration_command.rst - - esbonio.lsp.rst - Returns the server's current configuration. - - .. code-block:: json - - { - "server": { - "logLevel": "debug", - "logFilter": [], - "hideSphinxOutput": false - } - } - - - esbonio.lsp.sphinx - .. include:: ./advanced/_esbonio.lsp.sphinx_configuration_command.rst - -.. command:: esbonio.server.preview - - .. relevant-to:: Startup Module - - esbonio - .. include:: ./advanced/_esbonio.lsp.sphinx_preview_command.rst - - esbonio.lsp.rst - Currently a placeholder. - - esbonio.lsp.sphinx - .. include:: ./advanced/_esbonio.lsp.sphinx_preview_command.rst - - - -Notifications -------------- - -The bundled language servers also emit custom notifications that language clients -can use to react to events happening within the server. - -.. relevant-to:: Startup Module - - esbonio - .. include:: ./advanced/_esbonio.lsp.sphinx_notifications.rst - - esbonio.lsp.rst - Currently this server implements no custom notifications. - - esbonio.lsp.sphinx - .. include:: ./advanced/_esbonio.lsp.sphinx_notifications.rst diff --git a/docs/lsp/advanced/_esbonio.lsp.sphinx_build_command.rst b/docs/lsp/advanced/_esbonio.lsp.sphinx_build_command.rst deleted file mode 100644 index cf2237b3c..000000000 --- a/docs/lsp/advanced/_esbonio.lsp.sphinx_build_command.rst +++ /dev/null @@ -1 +0,0 @@ -Trigger a Sphinx build. diff --git a/docs/lsp/advanced/_esbonio.lsp.sphinx_configuration_command.rst b/docs/lsp/advanced/_esbonio.lsp.sphinx_configuration_command.rst deleted file mode 100644 index 26c7d615c..000000000 --- a/docs/lsp/advanced/_esbonio.lsp.sphinx_configuration_command.rst +++ /dev/null @@ -1,36 +0,0 @@ -Returns the server's current configuration. - -.. code-block:: json - - { - "config": { - "sphinx": { - "buildDir": "/home/.../docs/_build/html", - "builderName": "html", - "confDir": "/home/.../docs", - "configOverrides": {}, - "doctreeDir": "/home/.../docs/_build/doctrees", - "forceFullBuild": false, - "keepGoing": false, - "makeMode": true, - "numJobs": 1, - "quiet": false, - "silent": false, - "srcDir": "/home/.../docs", - "tags": [], - "verbosity": 0, - "warningIsError": false, - "command": [ - "sphinx-build", "-M", "html", "./docs", "./docs/_build", - ], - "version": "4.4.0" - }, - "server": { - "logLevel": "debug", - "logFilter": [], - "hideSphinxOutput": false - } - }, - "error": false, - "warnings": 1 - } diff --git a/docs/lsp/advanced/_esbonio.lsp.sphinx_notifications.rst b/docs/lsp/advanced/_esbonio.lsp.sphinx_notifications.rst deleted file mode 100644 index 306aeb301..000000000 --- a/docs/lsp/advanced/_esbonio.lsp.sphinx_notifications.rst +++ /dev/null @@ -1,31 +0,0 @@ - -``esbonio/buildStart`` - Emitted whenever a Sphinx build is started. - - .. code-block:: json - - {} - -``esbonio/buildComplete`` - Emitted whenever a Sphinx build is complete. - - .. code-block:: json - - { - "config": { - "sphinx": { - "version": "4.4.0", - "confDir": "/home/.../docs", - "srcDir": "/home/.../docs", - "buildDir": "/home/.../docs/_build/html", - "builderName": "html" - }, - "server": { - "log_level": "debug", - "log_filter": [], - "hide_sphinx_output": false - } - }, - "error": false, - "warnings": 0 - } diff --git a/docs/lsp/advanced/_esbonio.lsp.sphinx_preview_command.rst b/docs/lsp/advanced/_esbonio.lsp.sphinx_preview_command.rst deleted file mode 100644 index b3caf34aa..000000000 --- a/docs/lsp/advanced/_esbonio.lsp.sphinx_preview_command.rst +++ /dev/null @@ -1,23 +0,0 @@ -Start a local preview webserver. - -The server will spin up a local :mod:`python:http.server` on a random port in the -project's configured :confval:`buildDir ` which can be used to -preview the result of building the project. The server will return an object like the -following. - -.. code-block:: json - - { "port": 12345 } - -This command also accepts a parameters object with the following structure - -.. code-block:: json - - { "show": true } - -By default the ``show`` parameter will default to ``true`` which means the server will -also send a :lsp:`window/showDocument` request, asking the client to open the preview in a -web browser. - -If a client wants to implement its own preview mechanism (like the `VSCode Extension `_) -it can set ``show`` to ``false`` to suppress this behavior. diff --git a/docs/lsp/editors/nvim-lspconfig/init.vim b/docs/lsp/editors/nvim-lspconfig/init.vim index ff99a205d..19ab5713e 100644 --- a/docs/lsp/editors/nvim-lspconfig/init.vim +++ b/docs/lsp/editors/nvim-lspconfig/init.vim @@ -85,8 +85,11 @@ lspconfig.esbonio.setup { -- VSCode style output window. cmd = { 'lsp-devtools', 'agent', '--port', LSP_DEVTOOLS_PORT, '--', 'esbonio' }, init_options = { - server = { - logLevel = 'debug', + logging = { + level = 'debug', + -- Redirect logging output to window/logMessage notifications so that lsp-devtools can capture it. + stderr = false, + window = true, } }, settings = { diff --git a/docs/lsp/extending.rst b/docs/lsp/extending.rst index 7ddf6a7a1..362a8d438 100644 --- a/docs/lsp/extending.rst +++ b/docs/lsp/extending.rst @@ -3,68 +3,4 @@ Extending ========= -In order to support the extensible nature of reStructuredText and Sphinx, Esbonio itself is structured so that it can be easily extended. -This section of the documentation outlines the server's architecture and how you can write your own extensions. - -.. toctree:: - :maxdepth: 1 - - extending/directives - extending/roles - extending/api-reference - -.. _lsp_architecture: - -Architecture ------------- - -.. include:: ./_architecture.rst - -.. glossary:: - - Language Server - A language server is a subclass of the ``LanguageServer`` class provided by the `pygls`_ library. - - In Esbonio, all the features you would typically associate with a language server, e.g. completions are not actually implemented by the language server. - These features are provided through a number of "language features" (see below). - Instead a language server acts a container for all the active language features and provides an API they can use to query aspects of the environment. - - Esbonio currently provides two language servers - - - :class:`~esbonio.lsp.rst.RstLanguageServer`: Base language server, meant for "vanilla" docutils projects. - - :class:`~esbonio.lsp.sphinx.SphinxLanguageServer` Language server, specialising in Sphinx projects. - - Language Feature - Language features are subclasses of :class:`~esbonio.lsp.rst.LanguageFeature`. - They are typically based on a single aspect of reStructuredText (e.g. :class:`~esbonio.lsp.roles.Roles`). - - Language Features (where it makes sense) should be server agnostic, that way the same features can be reused across different envrionments. - - Engine - For lack of a better name... an "engine" is responsible for mapping messages from the LSP Protocol into function calls within the language server. - Unlike the other components of the architecture, an "engine" isn't formally defined and there is no API to implement. - Instead it's just the term used to refer to all the ``@server.feature()`` handlers that define how LSP messages should be handled. - - Currently we provide just a single "engine" :func:`~esbonio.lsp.create_language_server`. - As an example, here is how it handles ``textDocument/completion`` requests. - - .. literalinclude:: ../../lib/esbonio/esbonio/lsp/__init__.py - :language: python - :dedent: - :start-after: # - :end-before: # - - There is nothing in Esbonio that would prevent you from writing your own if you so desired. - - Extension Module - Ordinary Python modules are used to group related functionality together. - Taking inspiration from how Sphinx is architected, language servers are assembled by passing the list of modules to load to the :func:`~esbonio.lsp.create_language_server`. - This assembly process calls any functions with the name ``esbonio_setup`` allowing for ``LanguageFeatures`` to be configured and loaded into the server. - - Startup Module - As mentioned above, language servers are assembled and this is done inside a startup module. - A startup module in Esbonio is any Python script or module runnable by a ``python -m `` command that results in a running language server. - A good use case for a custom entry point would be starting up a language server instance pre configured with all the extensions required by your project. - - -.. _pygls: https://pygls.readthedocs.io/en/latest/index.html +Coming soon\ :sup:`TM` diff --git a/docs/lsp/extending/api-reference.rst b/docs/lsp/extending/api-reference.rst deleted file mode 100644 index 25314b70a..000000000 --- a/docs/lsp/extending/api-reference.rst +++ /dev/null @@ -1,71 +0,0 @@ -API Reference -============= - -.. warning:: - - While we will try not to break the API outlined below, until the language server - reaches ``v1.0`` we do not offer any stability guarantees. - -Language Servers ----------------- - -.. autofunction:: esbonio.lsp.create_language_server - -RstLanguageServer -^^^^^^^^^^^^^^^^^ - -.. autoclass:: esbonio.lsp.rst.RstLanguageServer - :members: - :show-inheritance: - -.. autoclass:: esbonio.lsp.rst.InitializationOptions - :members: - -.. autoclass:: esbonio.lsp.rst.config.ServerConfig - :members: - -.. autoclass:: esbonio.lsp.rst.config.ServerCompletionConfig - :members: - - -SphinxLanguageServer -^^^^^^^^^^^^^^^^^^^^ - -.. currentmodule:: esbonio.lsp.sphinx - -.. autoclass:: SphinxLanguageServer - :members: - :show-inheritance: - -.. autoclass:: InitializationOptions - :members: - -.. autoclass:: SphinxServerConfig - :members: - -.. autoclass:: SphinxConfig - :members: - -.. autoclass:: MissingConfigError - -Language Features ------------------ - -.. autoclass:: esbonio.lsp.LanguageFeature - :members: - -.. autoclass:: esbonio.lsp.CompletionContext - :members: - -.. autoclass:: esbonio.lsp.DefinitionContext - :members: - -.. autoclass:: esbonio.lsp.DocumentLinkContext - :members: - - -Testing -------- - -.. automodule:: esbonio.lsp.testing - :members: diff --git a/docs/lsp/extending/directives.rst b/docs/lsp/extending/directives.rst deleted file mode 100644 index 253e164cc..000000000 --- a/docs/lsp/extending/directives.rst +++ /dev/null @@ -1,48 +0,0 @@ -Directives -========== - -How To Guides -------------- - -The following guides outline how to extend the language server to add support for your custom directives. - -.. toctree:: - :glob: - :maxdepth: 1 - - directives/* - -API Reference -------------- - -.. currentmodule:: esbonio.lsp.directives - -.. autoclass:: Directives - :members: add_argument_completion_provider, - add_argument_definition_provider, - add_argument_link_provider, - add_documentation, - add_feature, - get_directives, - get_documentation, - get_implementation, - suggest_directives, - suggest_options - -.. autoclass:: DirectiveLanguageFeature - :members: - -.. autodata:: esbonio.lsp.util.patterns.DIRECTIVE - :no-value: - -.. autodata:: esbonio.lsp.util.patterns.DIRECTIVE_OPTION - :no-value: - -.. autoclass:: ArgumentCompletion - :members: - -.. autoclass:: ArgumentDefinition - :members: - -.. autoclass:: ArgumentLink - :members: diff --git a/docs/lsp/extending/directives/directive-registry.rst b/docs/lsp/extending/directives/directive-registry.rst deleted file mode 100644 index d74a490db..000000000 --- a/docs/lsp/extending/directives/directive-registry.rst +++ /dev/null @@ -1,122 +0,0 @@ -Supporting Custom Directive Registries -====================================== - -.. currentmodule:: esbonio.lsp.directives - -This guide walks through the process of teaching the language server how to discover directives stored in a custom registry. -Once complete, the following LSP features should start working with your directives. - -- Basic directive completions i.e. ``.. directive-name::`` but no argument completions. -- Basic option key completions i.e. ``:option-name:`` assuming options are declared in a directive's ``option_spec``, but no option value completions. -- Documentation hovers assuming you've provided documentation. -- Goto Implementation. - -.. note:: - - You may not need this guide. - - If you're registering your directive directly with - `docutils `__ or - `sphinx `__, - or using a `custom domain `__ - then you should find that the language server already has basic support for your custom directives out of the box. - - This guide is indended for adding support for directives that are not registered in a standard location. - -Still here? Great! Let's get started. - -Indexing Directives -------------------- - -As an example, we'll walk through the steps required to add (basic) support for Sphinx domains to the language server. - -.. note:: - - For the sake of brevity, some details have been omitted from the code examples below. - - If you're interested, you can find the actual implementation of the ``DomainDirectives`` class - `here `__. - -So that the server can discover the available directives, we have to provide a :class:`DirectiveLanguageFeature` that implements the :meth:`~DirectiveLanguageFeature.index_directives` method. -This method should return a dictionary where the keys are the canonical name of a directive which map to the class that implements it:: - - class DomainDirectives(DirectiveLanguageFeature): - def __init__(self, app: Sphinx): - self.app = app # Sphinx application instance. - - def index_directives(self) -> Dict[str, Type[Directive]]: - directives = {} - for prefix, domain in self.app.domains.items(): - for name, directive in domain.directives.items(): - directives[f"{prefix}:{name}"] = directive - - return directives - -In the case of Sphinx domains a directive's canonical name is of the form ``:`` e.g. ``py:function`` or ``c:macro``. - -This is the bare minimum required to make the language server aware of your custom directives, in fact if you were to try the above implementation you would already find completions being offered for domain based directives. -However, you would also notice that the short form of directives (e.g. ``function``) in the :ref:`standard ` and :confval:`primary ` domains are not included in the list of completions - despite being valid. - -To remedy this, you might be tempted to start adding multiple entries to the dictionary, one for each valid name **do not do this.** -Instead you can implement the :meth:`~DirectiveLanguageFeature.suggest_directives` method which solves this exact use case. - -.. tip:: - - If you want to play around with your own version of the ``DomainDirectives`` class you can disable the built in version by: - - - Passing the ``--exclude esbonio.lsp.sphinx.domains`` cli option, or - - If you're using VSCode adding ``esbonio.lsp.sphinx.domains`` to the :confval:`esbonio.server.excludedModules (string[])` option. - -(Optional) Suggesting Directives --------------------------------- - -The :meth:`~DirectiveLanguageFeature.suggest_directives` method is called each time the server is generating directive completions. -It can be used to tailor the list of directives that are offered to the user, depending on the current context. -Each ``DirectiveLanguageFeature`` has a default implementation, which may be sufficient depending on your use case:: - - def suggest_directives(self, context: CompletionContext) -> Iterable[Tuple[str, Type[Directive]]]: - return self.index_directives().items() - -However, in the case of Sphinx domains, we need to modify this to also include the short form of the directives in the standard and primary domains:: - - def suggest_directives(self, context: CompletionContext) -> Iterable[Tuple[str, Type[Directive]]]: - directives = self.index_directives() - primary_domain = self.app.config.primary_domain - - for key, directive in directives.items(): - - if key.startswith("std:"): - directives[key.replace("std:", "")] = directive - - if primary_domain and key.startswith(f"{primary_domain}:"): - directives[key.replace(f"{primary_domain}:", "")] = directive - - return directives.items() - -Now if you were to try this version, the short forms of the relevant directives would be offered as completion suggestions, but you would also notice that features like documentation hovers still don't work. -This is due to the language server not knowing which class implements these short form directives. - -(Optional) Implementation Lookups ---------------------------------- - -The :meth:`~DirectiveLanguageFeature.get_implementation` method is used by the language server to take a directive's name and lookup its implementation. -This powers features such as documentation hovers and goto implementation. -As with ``suggest_directives``, each ``DirectiveLanguageFeature`` has a default implementation which may be sufficient for your use case:: - - def get_implementation(self, directive: str, domain: Optional[str]) -> Optional[Type[Directive]]: - return self.index_directives().get(directive, None) - -In the case of Sphinx domains, if we see a directive without a domain prefix we need to see if it belongs to the standard or primary domains:: - - def get_implementation(self, directive: str, domain: Optional[str]) -> Optional[Type[Directive]]: - directives = self.index_directives() - - if domain is not None: - return directives.get(f"{domain}:{directive}", None) - - primary_domain = self.app.config.primary_domain - impl = directives.get(f"{primary_domain}:{directive}", None) - if impl is not None: - return impl - - return directives.get(f"std:{directive}", None) diff --git a/docs/lsp/extending/roles.rst b/docs/lsp/extending/roles.rst deleted file mode 100644 index 9253104ac..000000000 --- a/docs/lsp/extending/roles.rst +++ /dev/null @@ -1,49 +0,0 @@ -Roles -===== - -How To Guides -------------- - -The following guides outlne how to extens the language server to add support for your custom roles. - -.. toctree:: - :glob: - :maxdepth: 1 - - roles/* - -API Reference -------------- - -.. currentmodule:: esbonio.lsp.roles - -.. autoclass:: Roles - :members: add_documentation, - add_feature, - add_target_completion_provider, - add_target_definition_provider, - add_target_link_provider, - get_documentation, - get_implementation, - get_roles, - resolve_target_link, - suggest_roles, - suggest_targets - -.. autoclass:: RoleLanguageFeature - :members: - -.. autodata:: esbonio.lsp.util.patterns.ROLE - :no-value: - -.. autodata:: esbonio.lsp.util.patterns.DEFAULT_ROLE - :no-value: - -.. autoclass:: TargetDefinition - :members: - -.. autoclass:: TargetCompletion - :members: - -.. autoclass:: TargetLink - :members: diff --git a/docs/lsp/extending/roles/role-registry.rst b/docs/lsp/extending/roles/role-registry.rst deleted file mode 100644 index 12ae2e658..000000000 --- a/docs/lsp/extending/roles/role-registry.rst +++ /dev/null @@ -1,123 +0,0 @@ -Supporting Custom Role Registries -================================= - -.. currentmodule:: esbonio.lsp.roles - -This guide walks through the process of teaching the language server how to discover roles stored in a custom registry. -Once complete, the following LSP features should start working with your roles. - -- Basic role completions i.e. ``:role-name:`` but no target completions. -- Documentation hovers (assuming you've provided documentation) -- Goto Implementation - -.. note:: - - You may not need this guide. - - If you're registering your role directly with - `docutils `__ or - `sphinx `__, - or using a `custom domain `__ - then you should find that the language server already has basic support for your custom roles out of the box. - - This guide is indended for adding support for roles that are not registered in a standard location. - -Still here? Great! Let's get started. - -Indexing Roles --------------- - -As an example, we'll walk through the steps required to add (basic) support for Sphinx domains to the language server. - -.. note:: - - For the sake of brevity, some details have been omitted from the code examples below. - - If you're interested, you can find the actual implementation of the ``DomainRoles`` class - `here `__. - -So that the server can discover the available roles, we have to provide a :class:`RoleLanguageFeature` that implements the :meth:`~RoleLanguageFeature.index_roles` method. -This method should return a dictionary where the keys are the canonical name of the role which map to the function that implements it:: - - class DomainRoles(RoleLanguageFeature): - def __init__(self, app: Sphinx): - self.app = app # Sphinx application instance. - - def index_roles(self) -> Dict[str, Any]: - roles = {} - for prefix, domain in self.app.domains.items(): - for name, role in domain.roles.items(): - roles[f"{prefix}:{name}"] = role - - return roles - -In the case of Sphinx domains a role's canonical name is of the form ``:`` e.g. ``py:func`` or ``c:macro``. - -This is the bare minimum required to make the language server aware of your custom roles, in fact if you were to try the above implementation you would already find completions being offered for domain based roles. -However, you would also notice that the short form of roles (e.g. ``func``) in the :ref:`standard ` and :confval:`primary ` domains are not included in the list of completions - despite being valid. - -To remedy this, you might be tempted to start adding multiple entries to the dictionary, one for each valid name **do not do this.** -Instead you can implement the :meth:`~RoleLanguageFeature.suggest_roles` method which solves this exact use case. - -.. tip:: - - If you want to play around with your own version of the ``DomainRoles`` class you can disable the built in version by: - - - Passing the ``--exclude esbonio.lsp.sphinx.domains`` cli option, or - - If you're using VSCode adding ``esbonio.lsp.sphinx.domains`` to the :confval:`esbonio.server.excludedModules (string[])` option. - -(Optional) Suggesting Roles ---------------------------- - -The :meth:`~RoleLanguageFeature.suggest_roles` method is called each time the server is generating role completions. -It can be used to tailor the list of roles that are offered to the user, depending on the current context. -Each ``RoleLanguageFeature`` has a default implementation, which may be sufficient depending on your use case:: - - def suggest_roles(self, context: CompletionContext) -> Iterable[Tuple[str, Any]]: - """Suggest roles that may be used, given a completion context.""" - return self.index_roles().items() - -However, in the case of Sphinx domains, we need to modify this to also include the short form of the roles in the standard and primary domains:: - - def suggest_roles(self, context: CompletionContext) -> Iterable[Tuple[str, Any]]: - roles = self.index_roles() - primary_domain = self.app.config.primary_domain - - for key, role in roles.items(): - - if key.startswith("std:"): - roles[key.replace("std:", "")] = role - - if primary_domain and key.startswith(f"{primary_domain}:"): - roles[key.replace(f"{primary_domain}:", "")] = role - - return roles.items() - -Now if you were to try this version, the short forms of the relevant directives would be offered as completion suggestions, but you would also notice that features like documentation hovers still don't work. -This is due to the language server not knowing which class implements these short form directives. - -(Optional) Implementation Lookups ---------------------------------- - -The :meth:`~RoleLanguageFeature.get_implementation` method is used by the language server to take a role's name and lookup its implementation. -This powers features such as documentation hovers and goto implementation. -As with ``suggest_roles``, each ``RoleLanguageFeature`` has a default implementation which may be sufficient for your use case:: - - def get_implementation(self, role: str, domain: Optional[str]) -> Optional[Any]: - """Return the implementation for the given role name.""" - return self.index_roles().get(role, None) - -In the case of Sphinx domains, if we see a directive without a domain prefix we need to see if it belongs to the standard or primary domains:: - - def get_implementation(self, role: str, domain: Optional[str]) -> Optional[Any]: - roles = self.index_roles() - - if domain is not None: - return roles.get(f"{domain}:{role}", None) - - primary_domain = self.app.config.primary_domain - impl = roles.get(f"{primary_domain}:{role}", None) - if impl is not None: - return impl - - return roles.get(f"std:{role}", None) diff --git a/docs/lsp/getting-started.rst b/docs/lsp/getting-started.rst index f9aad42a5..86665dbfc 100644 --- a/docs/lsp/getting-started.rst +++ b/docs/lsp/getting-started.rst @@ -1,4 +1,4 @@ -.. _lsp_getting_started: +.. _lsp-getting-started: Getting Started =============== diff --git a/docs/lsp/reference.rst b/docs/lsp/reference.rst index 3e9b42173..cd48bea73 100644 --- a/docs/lsp/reference.rst +++ b/docs/lsp/reference.rst @@ -1,3 +1,5 @@ +.. _lsp-reference: + Reference ========= diff --git a/docs/lsp/reference/configuration.rst b/docs/lsp/reference/configuration.rst index 20947d844..d377e60c6 100644 --- a/docs/lsp/reference/configuration.rst +++ b/docs/lsp/reference/configuration.rst @@ -32,10 +32,193 @@ Below are all the configuration options supported by the server and their effect - :ref:`lsp-configuration-completion` - :ref:`lsp-configuration-developer` -- :ref:`lsp-configuration-server` +- :ref:`lsp-configuration-logging` - :ref:`lsp-configuration-sphinx` - :ref:`lsp-configuration-preview` +.. _lsp-configuration-completion: + +Completion +^^^^^^^^^^ + +The following options affect completion suggestions. + +.. esbonio:config:: esbonio.server.completion.preferredInsertBehavior + :scope: global + :type: string + + Controls how completions behave when accepted, the following values are supported. + + - ``replace`` (default) + + Accepted completions will replace existing text, allowing the server to rewrite the current line in place. + This allows the server to return all possible completions within the current context. + In this mode the server will set the ``textEdit`` field of a ``CompletionItem``. + + - ``insert`` + + Accepted completions will append to existing text rather than replacing it. + Since rewriting is not possible, only the completions that are compatible with any existing text will be returned. + In this mode the server will set the ``insertText`` field of a ``CompletionItem`` which should work better with editors that do no support ``textEdits``. + +.. _lsp-configuration-developer: + +Developer +^^^^^^^^^ + +The following options are useful when extending or working on the language server + +.. esbonio:config:: esbonio.server.showDeprecationWarnings + :scope: global + :type: boolean + + Developer flag which, when enabled, the server will publish any deprecation warnings as diagnostics. + +.. esbonio:config:: esbonio.server.enableDevTools (boolean) + :scope: global + :type: boolean + + Enable `lsp-devtools`_ integration for the language server itself. + +.. esbonio:config:: esbonio.sphinx.enableDevTools (boolean) + :scope: global + :type: boolean + + Enable `lsp-devtools`_ integration for the Sphinx subprocess started by the language server. + +.. esbonio:config:: esbonio.sphinx.pythonPath (string[]) + :scope: global + :type: string[] + + List of paths to use when constructing the value of ``PYTHONPATH``. + Used to inject the sphinx agent into the target environment." + +.. _lsp-devtools: https://swyddfa.github.io/lsp-devtools/docs/latest/en/ + +.. _lsp-configuration-logging: + +Logging +^^^^^^^ + +The following options control the logging output of the language server. + +.. esbonio:config:: esbonio.logging.level + :scope: global + :type: string + + Sets the default level of log messages emitted by the server. + The following values are accepted, sorted in the order from least to most verbose. + + - ``critical`` + - ``fatal`` + - ``error`` (default) + - ``warning`` + - ``info`` + - ``debug`` + +.. esbonio:config:: esbonio.logging.format + :scope: global + :type: string + + Sets the default format string to apply to log messages. + This can be any valid :external:ref:`%-style ` format string, referencing valid :external:ref:`logrecord-attributes` + + **Default value:** ``[%(name)s]: %(message)s`` + +.. esbonio:config:: esbonio.logging.filepath + :scope: global + :type: string + + If set, record log messages in the given filepath (relative to the server's working directory) + +.. esbonio:config:: esbonio.logging.stderr + :scope: global + :type: boolean + + If ``True`` (the default), the server will print log messages to the process' stderr + +.. esbonio:config:: esbonio.logging.window + :scope: global + :type: boolean + + If ``True``, the server will send messages to the client as :lsp:`window/logMessage` notifications + +.. esbonio:config:: esbonio.logging.config + :scope: global + :type: object + + This is an object used to override the default logging configuration for specific, named loggers. + Keys in the object are the names of loggers to override, values are a dictionary that can contain the following fields + + - ``level`` if present, overrides the value of :esbonio:conf:`esbonio.logging.level` + - ``format`` if present, overrides the value of :esbonio:conf:`esbonio.logging.format` + - ``filepath`` if present, overrides the value of :esbonio:conf:`esbonio.logging.filepath` + - ``stderr`` if present, overrides the value of :esbonio:conf:`esbonio.logging.stderr` + - ``window`` if present, overrides the value of :esbonio:conf:`esbonio.logging.window` + +Examples +"""""""" + +.. highlight:: json + +The following is equivalent to the server's default logging configuration:: + + { + "esbonio": { + "logging": { + "level": "error", + "format": "[%(name)s]: %(message)s", + "stderr": true, + "config": { + "sphinx": { + "level": "info", + "format": "%(message)s" + } + } + } + } + } + +This sets the default log level to ``debug`` and dials back or redirects the output from some of the noisier loggers:: + + { + "esbonio": { + "logging": { + "level": "debug", + "config": { + "esbonio.Configuration": { + "level": "info" + }, + "esbonio.PreviewServer": { + "filename": "http.log", + "stderr": false + }, + "esbonio.WebviewServer": { + "level": "error" + } + } + } + } + } + +Loggers +""""""" + +The following table summarises (some of) the available loggers and the type of messages they report + +========================== =========== +Name Description +========================== =========== +``esbonio`` Messages coming from ``esbonio`` itself that do not belong anywhere else +``esbonio.Configuration`` Messages about merging configuration from multiple sources and notifying the rest of the server when values change. +``esbonio.PreviewManager`` Messages from the component orchestrating the HTTP and Websocket servers that power the preview functionality +``esbonio.PreviewServer`` Records the HTTP traffic from the server that serves the HTML files built by Sphinx +``esbonio.SphinxManager`` Messages from the component that manages the server's underlying Sphinx processes +``esbonio.WebviewServer`` Messages about the websocket connection between the HTML viewer and the server +``py.warnings`` Log messages coming from Python's warnings framework +``sphinx`` Log messages coming from an underlying sphinx process +========================== =========== + .. _lsp-configuration-sphinx: Sphinx @@ -125,95 +308,11 @@ The following options control the behavior of the preview :type: integer The port number to bind the HTTP server to. - If ``0``, a random port number will be chosen". + If ``0`` (the default), a random port number will be chosen .. esbonio:config:: esbonio.preview.wsPort :scope: project :type: integer The port number to bind the WebSocket server to. - If ``0``, a random port number will be chosen" - -.. _lsp-configuration-server: - -Server -^^^^^^ - -The following options control the behavior of the language server as a whole. - -.. esbonio:config:: esbonio.server.logLevel - :scope: global - :type: string - - This can be used to set the level of log messages emitted by the server. - This can be set to one of the following values. - - - ``error`` (default) - - ``info`` - - ``debug`` - -.. esbonio:config:: esbonio.server.logFilter - :scope: global - :type: string[] - - The language server will typically include log output from all of its components. - This option can be used to restrict the log output to be only those named. - -.. _lsp-configuration-completion: - -Completion -^^^^^^^^^^ - -The following options affect completion suggestions. - -.. esbonio:config:: esbonio.server.completion.preferredInsertBehavior - :scope: global - :type: string - - Controls how completions behave when accepted, the following values are supported. - - - ``replace`` (default) - - Accepted completions will replace existing text, allowing the server to rewrite the current line in place. - This allows the server to return all possible completions within the current context. - In this mode the server will set the ``textEdit`` field of a ``CompletionItem``. - - - ``insert`` - - Accepted completions will append to existing text rather than replacing it. - Since rewriting is not possible, only the completions that are compatible with any existing text will be returned. - In this mode the server will set the ``insertText`` field of a ``CompletionItem`` which should work better with editors that do no support ``textEdits``. - -.. _lsp-configuration-developer: - -Developer -^^^^^^^^^ - -The following options are useful when extending or working on the language server - -.. esbonio:config:: esbonio.server.showDeprecationWarnings - :scope: global - :type: boolean - - Developer flag which, when enabled, the server will publish any deprecation warnings as diagnostics. - -.. esbonio:config:: esbonio.server.enableDevTools (boolean) - :scope: global - :type: boolean - - Enable `lsp-devtools`_ integration for the language server itself. - -.. esbonio:config:: esbonio.sphinx.enableDevTools (boolean) - :scope: global - :type: boolean - - Enable `lsp-devtools`_ integration for the Sphinx subprocess started by the language server. - -.. esbonio:config:: esbonio.sphinx.pythonPath (string[]) - :scope: global - :type: string[] - - List of paths to use when constructing the value of ``PYTHONPATH``. - Used to inject the sphinx agent into the target environment." - -.. _lsp-devtools: https://swyddfa.github.io/lsp-devtools/docs/latest/en/ + If ``0`` (the default), a random port number will be chosen diff --git a/docs/lsp/reference/notifications.rst b/docs/lsp/reference/notifications.rst new file mode 100644 index 000000000..3af90708f --- /dev/null +++ b/docs/lsp/reference/notifications.rst @@ -0,0 +1,18 @@ +Notifications +============= + +In addition to the language server protocol, Esbonio will emit the following notifications + +.. currentmodule:: esbonio.server.features.sphinx_manager.manager + +.. autoclass:: ClientCreatedNotification + :members: + +.. autoclass:: AppCreatedNotification + :members: + +.. autoclass:: ClientErroredNotification + :members: + +.. autoclass:: ClientDestroyedNotification + :members: diff --git a/flake.lock b/flake.lock index d11d4f426..8d9c47bca 100644 --- a/flake.lock +++ b/flake.lock @@ -10,11 +10,11 @@ ] }, "locked": { - "lastModified": 1704650820, - "narHash": "sha256-dRztpidI5eMB7u13IT4zD4ku6isWgQlheKSM1piPop4=", + "lastModified": 1712600859, + "narHash": "sha256-1a2djQq73lSl3MCQvNBGXrBWXbjr8uvqulRC47RG2ow=", "owner": "swyddfa", "repo": "lsp-devtools", - "rev": "13e5cfd55753e87157bc0a1beb7ce587b1bc8410", + "rev": "9bb50d3a19b12f0c4f98a5a8f564dee89d0c54cb", "type": "github" }, "original": { @@ -25,11 +25,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1704161960, - "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", + "lastModified": 1713128889, + "narHash": "sha256-aB90ZqzosyRDpBh+rILIcyP5lao8SKz8Sr2PSWvZrzk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "63143ac2c9186be6d9da6035fa22620018c85932", + "rev": "2748d22b45a99fb2deafa5f11c7531c212b2cefa", "type": "github" }, "original": { @@ -66,11 +66,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { diff --git a/lib/esbonio-extensions/esbonio/ext/spelling.py b/lib/esbonio-extensions/esbonio/ext/spelling.py index a0e554661..b98610e73 100644 --- a/lib/esbonio-extensions/esbonio/ext/spelling.py +++ b/lib/esbonio-extensions/esbonio/ext/spelling.py @@ -1,4 +1,5 @@ """Spell checking.""" + import re from typing import Dict from typing import List diff --git a/lib/esbonio-extensions/esbonio/relevant_to/__init__.py b/lib/esbonio-extensions/esbonio/relevant_to/__init__.py index f81fabb71..58427482b 100644 --- a/lib/esbonio-extensions/esbonio/relevant_to/__init__.py +++ b/lib/esbonio-extensions/esbonio/relevant_to/__init__.py @@ -39,8 +39,7 @@ def visit_relevant_to_script(self, node: relevant_to_script): self.body.append("") -def depart_relevant_to_script(self, node: relevant_to_script): - ... +def depart_relevant_to_script(self, node: relevant_to_script): ... class selection(nodes.Element): @@ -91,12 +90,10 @@ def visit_selection(self, node: selection): ) -def depart_selection(self, node: selection): - ... +def depart_selection(self, node: selection): ... -class relevant_section(nodes.Element): - ... +class relevant_section(nodes.Element): ... def visit_relevant_section(self, node: relevant_section): diff --git a/lib/esbonio/README.md b/lib/esbonio/README.md index 207479790..86b544f23 100644 --- a/lib/esbonio/README.md +++ b/lib/esbonio/README.md @@ -42,7 +42,7 @@ The language server provides the following features It's recommended to install the language server with [`pipx`](https://pipx.pypa.io/stable/) -Be sure to check out the [Getting Started](https://docs.esbon.io/latest/en/lsp/getting-started.html) guide for details on integrating the server with your editor of choice. +Be sure to check out the [Getting Started](https://docs.esbon.io/en/latest/lsp/getting-started.html) guide for details on integrating the server with your editor of choice. ``` $ pipx install esbonio diff --git a/lib/esbonio/changes/660.fix.md b/lib/esbonio/changes/660.fix.md new file mode 100644 index 000000000..e97bfad94 --- /dev/null +++ b/lib/esbonio/changes/660.fix.md @@ -0,0 +1 @@ +The server should no longer sometimes spawn duplicated Sphinx processes diff --git a/lib/esbonio/changes/695.fix.md b/lib/esbonio/changes/695.fix.md new file mode 100644 index 000000000..870753467 --- /dev/null +++ b/lib/esbonio/changes/695.fix.md @@ -0,0 +1 @@ +The server now respects Sphinx configuration values like `suppress_warnings` diff --git a/lib/esbonio/changes/718.fix.md b/lib/esbonio/changes/718.fix.md new file mode 100644 index 000000000..8a9e79c0c --- /dev/null +++ b/lib/esbonio/changes/718.fix.md @@ -0,0 +1 @@ +The server will no longer raise a `ValueError` when used in a situation where there is an empty workspace diff --git a/lib/esbonio/changes/748.breaking.md b/lib/esbonio/changes/748.breaking.md new file mode 100644 index 000000000..3327c279c --- /dev/null +++ b/lib/esbonio/changes/748.breaking.md @@ -0,0 +1,2 @@ +- Removed the `esbonio.server.logLevel` option, use `esbonio.logging.level` instead. +- Removed the `esbonio.server.logFilter` option, it has been made obselete by the other `esbonio.logging.*` options diff --git a/lib/esbonio/changes/748.enhancement.md b/lib/esbonio/changes/748.enhancement.md new file mode 100644 index 000000000..af44fadcd --- /dev/null +++ b/lib/esbonio/changes/748.enhancement.md @@ -0,0 +1,8 @@ +Added the following configuration options + +- `esbonio:config:: esbonio.logging.level`, set the default logging level of the server +- `esbonio:config:: esbonio.logging.format`, set the default format of server log messages +- `esbonio:config:: esbonio.logging.filepath`, enable logging to a file +- `esbonio:config:: esbonio.logging.stderr`, print log messages to stderr +- `esbonio:config:: esbonio.logging.window`, send log messages as `window/logMessage` notifications +- `esbonio:config:: esbonio.logging.config`, override logging configuration for individual loggers, see the [documentation](https://docs.esbon.io/en/latest/lsp/reference/configuration.html#lsp-configuration-logging) for details diff --git a/lib/esbonio/changes/750.enhancement.md b/lib/esbonio/changes/750.enhancement.md new file mode 100644 index 000000000..d2ca8fb41 --- /dev/null +++ b/lib/esbonio/changes/750.enhancement.md @@ -0,0 +1 @@ +The server will now automatically restart the underlying Sphinx process when it detects a change in its configuration diff --git a/lib/esbonio/changes/756.enhancement.md b/lib/esbonio/changes/756.enhancement.md new file mode 100644 index 000000000..46cc83076 --- /dev/null +++ b/lib/esbonio/changes/756.enhancement.md @@ -0,0 +1 @@ +The server now emits `sphinx/clientCreated`, `sphinx/clientErrored` and `sphinx/clientDestroyed` notifications that correspond to the lifecycle of the underlying Sphinx process diff --git a/lib/esbonio/esbonio/__main__.py b/lib/esbonio/esbonio/__main__.py index 4b62e28ca..888ed7eeb 100644 --- a/lib/esbonio/esbonio/__main__.py +++ b/lib/esbonio/esbonio/__main__.py @@ -1,4 +1,5 @@ """Default startup module, identical to calling ``python -m esbonio.server``""" + import sys from esbonio.server.cli import main diff --git a/lib/esbonio/esbonio/cli/__init__.py b/lib/esbonio/esbonio/cli/__init__.py deleted file mode 100644 index 1e1299114..000000000 --- a/lib/esbonio/esbonio/cli/__init__.py +++ /dev/null @@ -1,113 +0,0 @@ -import argparse -import logging -import sys -import warnings -from typing import Literal -from typing import Union - -from pygls.protocol import default_converter - - -def esbonio_converter(): - converter = default_converter() - converter.register_structure_hook(Union[Literal["auto"], int], lambda obj, _: obj) - - return converter - - -def setup_cli(progname: str, description: str) -> argparse.ArgumentParser: - """Return an argument parser with the default command line options required for - main. - """ - - cli = argparse.ArgumentParser(prog=f"python -m {progname}", description=description) - cli.add_argument( - "-p", - "--port", - type=int, - default=None, - help="start a TCP instance of the language server listening on the given port.", - ) - cli.add_argument( - "--version", action="store_true", help="print the current version and exit." - ) - - modules = cli.add_argument_group( - "modules", "include/exclude language server modules." - ) - modules.add_argument( - "-i", - "--include", - metavar="MOD", - action="append", - default=[], - dest="included_modules", - help="include an additional module in the server configuration, can be given multiple times.", - ) - modules.add_argument( - "-e", - "--exclude", - metavar="MOD", - action="append", - default=[], - dest="excluded_modules", - help="exclude a module from the server configuration, can be given multiple times.", - ) - - return cli - - -def main(cli: argparse.ArgumentParser): - """Standard main function for each of the default language servers.""" - - # Put these here to avoid circular import issues. - from esbonio.lsp import __version__ - from esbonio.lsp import create_language_server - from esbonio.lsp.log import LOG_NAMESPACE - from esbonio.lsp.log import MemoryHandler - - args = cli.parse_args() - - if args.version: - print(f"v{__version__}") - sys.exit(0) - - modules = list(args.modules) - - for mod in args.included_modules: - modules.append(mod) - - for mod in args.excluded_modules: - if mod in modules: - modules.remove(mod) - - # Ensure we can capture warnings. - logging.captureWarnings(True) - warnlog = logging.getLogger("py.warnings") - - if not sys.warnoptions: - warnings.simplefilter("default") # Enable capture of DeprecationWarnings - - # Setup a temporary logging handler that can cache messages until the language server - # is ready to forward them onto the client. - logger = logging.getLogger(LOG_NAMESPACE) - logger.setLevel(logging.DEBUG) - - handler = MemoryHandler() - handler.setLevel(logging.DEBUG) - logger.addHandler(handler) - warnlog.addHandler(handler) - - server = create_language_server( - args.server_cls, - modules, - name="esbonio", - version=__version__, - # TODO: Figure out how to make this extensible - converter_factory=esbonio_converter, - ) - - if args.port: - server.start_tcp("localhost", args.port) - else: - server.start_io() diff --git a/lib/esbonio/esbonio/cli/py.typed b/lib/esbonio/esbonio/cli/py.typed deleted file mode 100644 index 4a787c57d..000000000 --- a/lib/esbonio/esbonio/cli/py.typed +++ /dev/null @@ -1 +0,0 @@ -# esbonio.cli: marker file for PEP 561 diff --git a/lib/esbonio/esbonio/lsp/__init__.py b/lib/esbonio/esbonio/lsp/__init__.py deleted file mode 100644 index 9d30bb3cb..000000000 --- a/lib/esbonio/esbonio/lsp/__init__.py +++ /dev/null @@ -1,419 +0,0 @@ -import enum -import importlib -import json -import logging -import textwrap -import traceback -from typing import Any -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional -from typing import Type - -from lsprotocol.types import COMPLETION_ITEM_RESOLVE -from lsprotocol.types import INITIALIZE -from lsprotocol.types import INITIALIZED -from lsprotocol.types import SHUTDOWN -from lsprotocol.types import TEXT_DOCUMENT_CODE_ACTION -from lsprotocol.types import TEXT_DOCUMENT_COMPLETION -from lsprotocol.types import TEXT_DOCUMENT_DEFINITION -from lsprotocol.types import TEXT_DOCUMENT_DID_CHANGE -from lsprotocol.types import TEXT_DOCUMENT_DID_OPEN -from lsprotocol.types import TEXT_DOCUMENT_DID_SAVE -from lsprotocol.types import TEXT_DOCUMENT_DOCUMENT_LINK -from lsprotocol.types import TEXT_DOCUMENT_DOCUMENT_SYMBOL -from lsprotocol.types import TEXT_DOCUMENT_HOVER -from lsprotocol.types import TEXT_DOCUMENT_IMPLEMENTATION -from lsprotocol.types import WORKSPACE_DID_DELETE_FILES -from lsprotocol.types import CodeActionParams -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionList -from lsprotocol.types import CompletionOptions -from lsprotocol.types import CompletionParams -from lsprotocol.types import DefinitionParams -from lsprotocol.types import DeleteFilesParams -from lsprotocol.types import DidChangeTextDocumentParams -from lsprotocol.types import DidOpenTextDocumentParams -from lsprotocol.types import DidSaveTextDocumentParams -from lsprotocol.types import DocumentLinkParams -from lsprotocol.types import DocumentSymbolParams -from lsprotocol.types import FileOperationFilter -from lsprotocol.types import FileOperationPattern -from lsprotocol.types import FileOperationRegistrationOptions -from lsprotocol.types import Hover -from lsprotocol.types import HoverParams -from lsprotocol.types import ImplementationParams -from lsprotocol.types import InitializedParams -from lsprotocol.types import InitializeParams -from lsprotocol.types import MarkupContent -from lsprotocol.types import MarkupKind - -from .rst import CompletionContext -from .rst import DefinitionContext -from .rst import DocumentLinkContext -from .rst import HoverContext -from .rst import ImplementationContext -from .rst import LanguageFeature -from .rst import RstLanguageServer -from .symbols import SymbolVisitor - -__version__ = "0.16.2" - -__all__ = [ - "CompletionContext", - "DefinitionContext", - "DocumentLinkContext", - "HoverContext", - "ImplementationContext", - "LanguageFeature", - "RstLanguageServer", - "create_language_server", -] - -logger = logging.getLogger(__name__) - -# Commands -ESBONIO_SERVER_CONFIGURATION = "esbonio.server.configuration" -ESBONIO_SERVER_PREVIEW = "esbonio.server.preview" -ESBONIO_SERVER_BUILD = "esbonio.server.build" - - -def create_language_server( - server_cls: Type[RstLanguageServer], modules: Iterable[str], *args, **kwargs -) -> RstLanguageServer: - """Create a new language server instance. - - Parameters - ---------- - server_cls: - The class definition to create the server from. - modules: - The list of modules that should be loaded. - args, kwargs: - Any additional arguments that should be passed to the language server's - constructor. - """ - - if "logger" not in kwargs: - kwargs["logger"] = logger - - server = server_cls(*args, **kwargs) - - for module in modules: - _load_module(server, module) - - return _configure_lsp_methods(server) - - -def _configure_lsp_methods(server: RstLanguageServer) -> RstLanguageServer: - @server.feature(INITIALIZE) - def on_initialize(ls: RstLanguageServer, params: InitializeParams): - ls.initialize(params) - - for feature in ls._features.values(): - feature.initialize(params) - - @server.feature(INITIALIZED) - def on_initialized(ls: RstLanguageServer, params: InitializedParams): - ls.initialized(params) - - for feature in ls._features.values(): - feature.initialized(params) - - @server.feature(SHUTDOWN) - def on_shutdown(ls: RstLanguageServer, *args): - ls.on_shutdown(*args) - - for feature in ls._features.values(): - feature.on_shutdown(*args) - - @server.feature(TEXT_DOCUMENT_DID_OPEN) - def on_open(ls: RstLanguageServer, params: DidOpenTextDocumentParams): - ... - - @server.feature(TEXT_DOCUMENT_DID_CHANGE) - def on_change(ls: RstLanguageServer, params: DidChangeTextDocumentParams): - pass - - @server.command(ESBONIO_SERVER_BUILD) - def build(ls: RstLanguageServer, *args): - params = {} if not args[0] else args[0][0]._asdict() - force_all: bool = params.get("force_all", False) - filenames: Optional[List[str]] = params.get("filenames", None) - ls.build(force_all, filenames) - - @server.feature(TEXT_DOCUMENT_DID_SAVE) - def on_save(ls: RstLanguageServer, params: DidSaveTextDocumentParams): - ls.save(params) - - for feature in ls._features.values(): - feature.save(params) - - @server.feature( - WORKSPACE_DID_DELETE_FILES, - FileOperationRegistrationOptions( - filters=[ - FileOperationFilter( - pattern=FileOperationPattern(glob="**/*.rst"), - ) - ] - ), - ) - def on_delete_files(ls: RstLanguageServer, params: DeleteFilesParams): - ls.delete_files(params) - - for feature in ls._features.values(): - feature.delete_files(params) - - @server.feature(TEXT_DOCUMENT_CODE_ACTION) - def on_code_action(ls: RstLanguageServer, params: CodeActionParams): - actions = [] - - for feature in ls._features.values(): - actions += feature.code_action(params) - - return actions - - @server.feature(TEXT_DOCUMENT_HOVER) - def on_hover(ls: RstLanguageServer, params: HoverParams): - uri = params.text_document.uri - doc = ls.workspace.get_document(uri) - pos = params.position - line = ls.line_at_position(doc, pos) - location = ls.get_location_type(doc, pos) - - hover_values = [] - for feature in ls._features.values(): - for pattern in feature.hover_triggers: - for match in pattern.finditer(line): - if not match: - continue - - # only trigger hover if the position of the request is within - # the match - start, stop = match.span() - if start <= pos.character <= stop: - context = HoverContext( - doc=doc, - location=location, - match=match, - position=pos, - capabilities=ls.client_capabilities, - ) - ls.logger.debug("Hover context: %s", context) - - hover_value = feature.hover(context) - hover_values.append(hover_value) - - hover_content_values = "\n".join(hover_values) - - return Hover( - contents=MarkupContent( - kind=MarkupKind.Markdown, - value=hover_content_values, - ) - ) - - # - @server.feature( - TEXT_DOCUMENT_COMPLETION, - CompletionOptions( - trigger_characters=[">", ".", ":", "`", "<", "/"], resolve_provider=True - ), - ) - def on_completion(ls: RstLanguageServer, params: CompletionParams): - uri = params.text_document.uri - pos = params.position - - doc = ls.workspace.get_document(uri) - line = ls.line_at_position(doc, pos) - location = ls.get_location_type(doc, pos) - - items = [] - - for name, feature in ls._features.items(): - for pattern in feature.completion_triggers: - for match in pattern.finditer(line): - if not match: - continue - - # Only trigger completions if the position of the request is within - # the match. - start, stop = match.span() - if start <= pos.character <= stop: - context = CompletionContext( - doc=doc, - location=location, - match=match, - position=pos, - config=ls.user_config.server.completion, - capabilities=ls.client_capabilities, - ) - ls.logger.debug("Completion context: %s", context) - - for item in feature.complete(context): - item.data = {"source_feature": name, **(item.data or {})} # type: ignore - items.append(item) - - return CompletionList(is_incomplete=False, items=items) - - # - - @server.feature(COMPLETION_ITEM_RESOLVE) - def on_completion_resolve( - ls: RstLanguageServer, item: CompletionItem - ) -> CompletionItem: - source = (item.data or {}).get("source_feature", "") # type: ignore - feature = ls.get_feature(source) - - if not feature: - ls.logger.error( - "Unable to resolve completion item, unknown source: '%s'", source - ) - return item - - return feature.completion_resolve(item) - - @server.feature(TEXT_DOCUMENT_DEFINITION) - def on_definition(ls: RstLanguageServer, params: DefinitionParams): - uri = params.text_document.uri - pos = params.position - - doc = ls.workspace.get_document(uri) - line = ls.line_at_position(doc, pos) - location = ls.get_location_type(doc, pos) - - definitions = [] - - for feature in ls._features.values(): - for pattern in feature.definition_triggers: - for match in pattern.finditer(line): - if not match: - continue - - start, stop = match.span() - if start <= pos.character and pos.character <= stop: - context = DefinitionContext( - doc=doc, location=location, match=match, position=pos - ) - definitions += feature.definition(context) - - return definitions - - @server.feature(TEXT_DOCUMENT_IMPLEMENTATION) - def on_implementation(ls: RstLanguageServer, params: ImplementationParams): - uri = params.text_document.uri - pos = params.position - - doc = ls.workspace.get_document(uri) - line = ls.line_at_position(doc, pos) - location = ls.get_location_type(doc, pos) - - implementations = [] - - for feature in ls._features.values(): - for pattern in feature.implementation_triggers: - for match in pattern.finditer(line): - if not match: - continue - - start, stop = match.span() - if start <= pos.character and pos.character <= stop: - context = ImplementationContext( - doc=doc, location=location, match=match, position=pos - ) - ls.logger.debug("Implementation context: %s", context) - implementations += feature.implementation(context) - - return implementations - - @server.feature(TEXT_DOCUMENT_DOCUMENT_LINK) - def on_document_link(ls: RstLanguageServer, params: DocumentLinkParams): - uri = params.text_document.uri - doc = ls.workspace.get_document(uri) - context = DocumentLinkContext(doc=doc, capabilities=ls.client_capabilities) - - links = [] - for feature in ls._features.values(): - links += feature.document_link(context) or [] - - return links - - @server.feature(TEXT_DOCUMENT_DOCUMENT_SYMBOL) - def on_document_symbol(ls: RstLanguageServer, params: DocumentSymbolParams): - doctree = ls.get_initial_doctree(uri=params.text_document.uri) - if doctree is None: - return None - - visitor = SymbolVisitor(ls, doctree) - doctree.walkabout(visitor) - - return visitor.symbols - - @server.command(ESBONIO_SERVER_CONFIGURATION) - def get_configuration(ls: RstLanguageServer, *args): - """Get the server's configuration. - - Not to be confused with the ``workspace/configuration`` request where the server - can request the client's configuration. This is so clients can ask for sphinx's - output path for example. - - As far as I know, there isn't anything built into the spec to cater for this? - """ - config = ls.configuration - ls.logger.debug("%s: %s", ESBONIO_SERVER_CONFIGURATION, dump(config)) - - return config - - @server.command(ESBONIO_SERVER_PREVIEW) - def preview(ls: RstLanguageServer, *args) -> Dict[str, Any]: - """Start/Generate a preview of the project""" - params = {} if not args[0] else args[0][0] - ls.logger.debug("%s: %s", ESBONIO_SERVER_PREVIEW, params) - - return ls.preview(params) or {} - - return server - - -def _load_module(server: RstLanguageServer, modname: str): - """Load an extension module by calling its ``esbonio_setup`` function, if it exists.""" - - try: - module = importlib.import_module(modname) - except ImportError: - logger.error( - "Unable to import module '%s'\n%s", modname, traceback.format_exc() - ) - return None - - setup = getattr(module, "esbonio_setup", None) - if setup is None: - logger.error("Skipping module '%s', missing 'esbonio_setup' function", modname) - return None - - server.load_extension(modname, setup) - - -def dump(obj) -> str: - """Debug helper function that converts an object to JSON.""" - - def default(o): - if isinstance(o, enum.Enum): - return o.value - - fields = {} - for k, v in o.__dict__.items(): - if v is None: - continue - - # Truncate long strings - but not uris! - if isinstance(v, str) and not k.lower().endswith("uri"): - v = textwrap.shorten(v, width=25) - - fields[k] = v - - return fields - - return json.dumps(obj, default=default) diff --git a/lib/esbonio/esbonio/lsp/directives/__init__.py b/lib/esbonio/esbonio/lsp/directives/__init__.py deleted file mode 100644 index b62c836dc..000000000 --- a/lib/esbonio/esbonio/lsp/directives/__init__.py +++ /dev/null @@ -1,927 +0,0 @@ -import re -import traceback -import typing -import warnings -from typing import Any -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional -from typing import Protocol -from typing import Tuple -from typing import Type - -from docutils.parsers.rst import Directive -from lsprotocol.types import CompletionItem -from lsprotocol.types import DocumentLink -from lsprotocol.types import Location -from lsprotocol.types import MarkupContent -from lsprotocol.types import MarkupKind -from lsprotocol.types import Position -from lsprotocol.types import Range - -from esbonio.lsp import CompletionContext -from esbonio.lsp import DefinitionContext -from esbonio.lsp import DocumentLinkContext -from esbonio.lsp import HoverContext -from esbonio.lsp import ImplementationContext -from esbonio.lsp import LanguageFeature -from esbonio.lsp import RstLanguageServer -from esbonio.lsp.sphinx import SphinxLanguageServer -from esbonio.lsp.util.inspect import get_object_location -from esbonio.lsp.util.patterns import DIRECTIVE -from esbonio.lsp.util.patterns import DIRECTIVE_OPTION - -from .completions import render_directive_completion -from .completions import render_directive_option_completion - - -class DirectiveLanguageFeature: - """Base class for directive language features.""" - - def complete_arguments( - self, context: CompletionContext, domain: str, name: str - ) -> List[CompletionItem]: - """Return a list of completion items representing valid targets for the given - directive. - - Parameters - ---------- - context: - The completion context - domain: - The name of the domain the directive is a member of - name: - The name of the domain - """ - return [] - - def get_implementation( - self, directive: str, domain: Optional[str] - ) -> Optional[Type[Directive]]: - """Return the implementation for the given directive name.""" - return self.index_directives().get(directive, None) - - def index_directives(self) -> Dict[str, Type[Directive]]: - """Return all known directives.""" - return dict() - - def suggest_directives( - self, context: CompletionContext - ) -> Iterable[Tuple[str, Type[Directive]]]: - """Suggest directives that may be used, given a completion context.""" - return self.index_directives().items() - - def suggest_options( - self, context: CompletionContext, directive: str, domain: Optional[str] - ) -> Iterable[str]: - """Suggest options that may be used, given a completion context.""" - - impl = self.get_implementation(directive, domain) - if impl is None: - return [] - - option_spec = getattr(impl, "option_spec", {}) or {} - return option_spec.keys() - - def resolve_argument_link( - self, - context: DocumentLinkContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> Tuple[Optional[str], Optional[str]]: - """Resolve a document link request for the given argument. - - Parameters - ---------- - context - The context of the document link request. - - directive - The name of the directive the argument is associated with. - - domain - The name of the domain the directive belongs to, if applicable. - - argument - The argument to resolve the link for. - """ - return None, None - - def find_argument_definitions( - self, - context: DefinitionContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> List[Location]: - """Return a list of locations representing definitions of the given argument. - - Parameters - ---------- - context - The context of the definition request. - - directive - The name of the directive the argument is associated with. - - domain - The name of the domain the directive belongs to, if applicable. - - argument - The argument to find the definition of. - """ - return [] - - -class ArgumentCompletion(Protocol): - """A completion provider for directive arguments. - - .. deprecated:: 0.14.2 - - This will be removed in ``v1.0``, use subclasses of - :class:`~esbonio.lsp.directives.DirectiveLanguageFeature` instead. - """ - - def complete_arguments( - self, context: CompletionContext, domain: str, name: str - ) -> List[CompletionItem]: - """Return a list of completion items representing valid targets for the given - directive. - - Parameters - ---------- - context: - The completion context - domain: - The name of the domain the directive is a member of - name: - The name of the domain - """ - - -class ArgumentDefinition(Protocol): - """A definition provider for directive arguments. - - .. deprecated:: 0.14.2 - - This will be removed in ``v1.0``, use subclasses of - :class:`~esbonio.lsp.directives.DirectiveLanguageFeature` instead. - """ - - def find_definitions( - self, - context: DefinitionContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> List[Location]: - """Return a list of locations representing definitions of the given argument. - - Parameters - ---------- - context: - The context of the definition request. - directive: - The name of the directive the argument is associated with. - domain: - The name of the domain the directive belongs to, if applicable. - argument: - The argument to find the definition of. - """ - - -class ArgumentLink(Protocol): - """A document link resolver for directive arguments. - - .. deprecated:: 0.14.2 - - This will be removed in ``v1.0``, use subclasses of - :class:`~esbonio.lsp.directives.DirectiveLanguageFeature` instead. - """ - - def resolve_link( - self, - context: DocumentLinkContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> Tuple[Optional[str], Optional[str]]: - """Resolve a document link request for the given argument. - - Parameters - ---------- - context: - The context of the document link request. - directive: - The name of the directive the argument is associated with. - domain: - The name of the domain the directive belongs to, if applicable. - argument: - The argument to resolve the link for. - """ - - -class Directives(LanguageFeature): - """Directive support for the language server.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self._documentation: Dict[str, Dict[str, str]] = {} - """Cache for documentation.""" - - self._features: Dict[str, DirectiveLanguageFeature] = {} - """The collection of registered features.""" - - def add_feature(self, feature: DirectiveLanguageFeature): - """Register a directive language feature. - - Parameters - ---------- - feature - The directive language feature - """ - key = f"{feature.__module__}.{feature.__class__.__name__}" - - # Create an unique key for this instance. - if key in self._features: - key += f".{len([k for k in self._features.keys() if k.startswith(key)])}" - - self._features[key] = feature - - def add_argument_completion_provider(self, provider: ArgumentCompletion) -> None: - """Register an :class:`~esbonio.lsp.directives.ArgumentCompletion` provider. - - .. deprecated:: 0.14.2 - - This will be removed in ``v1.0``, use - :meth:`~esbonio.lsp.directives.Directives.add_feature` with a - :class:`~esbonio.lsp.directives.DirectiveLanguageFeature` subclass instead. - - Parameters - ---------- - provider: - The provider to register. - """ - warnings.warn( - "ArgumentCompletion providers are deprecated in favour of " - "DirectiveLanguageFeatures, this method will be removed in v1.0", - DeprecationWarning, - stacklevel=2, - ) - - name = provider.__class__.__name__ - key = f"{provider.__module__}.{name}.completion" - - # Automatically derive the feature definition from the provider. - feature = type( - f"{name}CompletionProvider", - (DirectiveLanguageFeature,), - {"complete_arguments": provider.complete_arguments}, - )() - - self._features[key] = feature - - def add_argument_definition_provider(self, provider: ArgumentDefinition) -> None: - """Register an :class:`~esbonio.lsp.directives.ArgumentDefinition` provider. - - .. deprecated:: 0.14.2 - - This will be removed in ``v1.0``, use - :meth:`~esbonio.lsp.directives.Directives.add_feature` with a - :class:`~esbonio.lsp.directives.DirectiveLanguageFeature` subclass instead. - - Parameters - ---------- - provider: - The provider to register. - """ - warnings.warn( - "ArgumentDefinition providers are deprecated in favour of " - "DirectiveLanguageFeatures, this method will be removed in v1.0", - DeprecationWarning, - stacklevel=2, - ) - - name = provider.__class__.__name__ - key = f"{provider.__module__}.{name}.definitions" - - # Automatically derive the feature definition from the provider. - feature = type( - f"{name}DefinitionProvider", - (DirectiveLanguageFeature,), - {"find_argument_definitions": provider.find_definitions}, - )() - - self._features[key] = feature - - def add_argument_link_provider(self, provider: ArgumentLink) -> None: - """Register an :class:`~esbonio.lsp.directives.ArgumentLink` provider. - - .. deprecated:: 0.14.2 - - This will be removed in ``v1.0``, use - :meth:`~esbonio.lsp.directives.Directives.add_feature` with a - :class:`~esbonio.lsp.directives.DirectiveLanguageFeature` subclass instead. - - Parameters - ---------- - provider: - The provider to register. - """ - warnings.warn( - "ArgumentLink providers are deprecated in favour of " - "DirectiveLanguageFeatures, this method will be removed in v1.0", - DeprecationWarning, - stacklevel=2, - ) - - name = provider.__class__.__name__ - key = f"{provider.__module__}.{name}.links" - - # Automatically derive the feature definition from the provider. - feature = type( - f"{name}LinkProvider", - (DirectiveLanguageFeature,), - {"resolve_argument_link": provider.resolve_link}, - )() - - self._features[key] = feature - - def add_documentation(self, documentation: Dict[str, Dict[str, Any]]) -> None: - """Register directive documentation. - - ``documentation`` should be a dictionary with the following structure :: - - documentation = { - "raw(docutils.parsers.rst.directives.misc.Raw)": { - "is_markdown": true, - "license": "https://...", - "source": "https://...", - "description": [ - "# .. raw::", - "The raw directive is used for...", - ... - ] - "options": { - "file": "The file option allows...", - ... - } - } - } - - where the key has the form ``name(dotted_name)``. There are cases where a - directive's implementation is not sufficient to uniquely identify it as - multiple directives can be provided by a single class. - - This means the key has to be a combination of the ``name`` the user writes - in a reStructuredText document and ``dotted_name`` is the fully qualified - class name of the directive's implementation. - - .. note:: - - If there is a clash with an existing key, the existing value will be - overwritten with the new value. - - The values in this dictionary are themselves dictionaries with the following - fields. - - ``description`` - A list of strings for the directive's main documentation. - - ``options``, - A dictionary, with a field for the documentaton of each of the directive's - options. - - ``is_markdown`` - A boolean flag used to indicate whether the ``description`` and ``options`` - are written in plain text or markdown. - - ``source`` - The url to the documentation's source - - ``license`` - The url to the documentation's license - - Parameters - ---------- - documentation: - The documentation to register. - """ - - for key, doc in documentation.items(): - description = doc.get("description", []) - - if not description: - continue - - source = doc.get("source", "") - if source: - description.append(f"\n[Source]({source})") - - license = doc.get("license", "") - if license: - description.append(f"\n[License]({license})") - - doc["description"] = "\n".join(description) - self._documentation[key] = doc - - def get_directives(self) -> Dict[str, Type[Directive]]: - """Return a dictionary of all known directives.""" - - directives = {} - - for name, feature in self._features.items(): - try: - directives.update(feature.index_directives()) - except Exception: - self.logger.error( - "Unable to index directives, error in feature '%s'\n%s", - name, - traceback.format_exc(), - ) - - return directives - - def get_implementation( - self, directive: str, domain: Optional[str] - ) -> Optional[Type[Directive]]: - """Return the implementation of a directive given its name - - Parameters - ---------- - directive - The name of the directive. - - domain - The domain of the directive, if applicable. - """ - - if domain: - name = f"{domain}:{directive}" - else: - name = directive - - for feature_name, feature in self._features.items(): - try: - impl = feature.get_implementation(directive, domain) - if impl is not None: - return impl - except Exception: - self.logger.error( - "Unable to get implementation for '%s', error in feature: '%s'\n%s", - name, - feature_name, - traceback.format_exc(), - ) - - self.logger.debug( - "Unable to get implementation for '%s', unknown directive", name - ) - return None - - def suggest_directives( - self, context: CompletionContext - ) -> Iterable[Tuple[str, Type[Directive]]]: - """Suggest directives that may be used, given a completion context. - - Parameters - ---------- - context - The CompletionContext. - """ - - for name, feature in self._features.items(): - try: - yield from feature.suggest_directives(context) - except Exception: - self.logger.error( - "Unable to suggest directives, error in feature: '%s'\n%s", - name, - traceback.format_exc(), - ) - - def suggest_options( - self, context: CompletionContext, directive: str, domain: Optional[str] - ) -> Iterable[str]: - """Suggest directive options that may be used, given a completion context.""" - - if domain: - name = f"{domain}:{directive}" - else: - name = directive - - for feature_name, feature in self._features.items(): - try: - yield from feature.suggest_options(context, directive, domain) - except Exception: - self.logger.error( - "Unable to suggest options for directive '%s', error in feature: '%s'\n%s", - name, - feature_name, - traceback.format_exc(), - ) - - completion_triggers = [DIRECTIVE, DIRECTIVE_OPTION] - definition_triggers = [DIRECTIVE] - hover_triggers = [DIRECTIVE] - implementation_triggers = [DIRECTIVE] - - def completion_resolve(self, item: CompletionItem) -> CompletionItem: - # We need extra info to know who to call. - if not item.data: - return item - - data = typing.cast(Dict, item.data) - ctype = data.get("completion_type", "") - - if ctype == "directive": - return self.completion_resolve_directive(item) - - if ctype == "directive_option": - return self.completion_resolve_option(item) - - return item - - def complete(self, context: CompletionContext) -> List[CompletionItem]: - # Do not suggest completions within the middle of Python code. - if context.location == "py": - return [] - - groups = context.match.groupdict() - - # Are we completing a directive's options? - if "directive" not in groups: - return self.complete_options(context) - - # Are we completing the directive's argument? - directive_end = context.match.span()[0] + len(groups["directive"]) - complete_directive = groups["directive"].endswith("::") - - if complete_directive and directive_end < context.position.character: - return self.complete_arguments(context) - - return self.complete_directives(context) - - def complete_arguments(self, context: CompletionContext) -> List[CompletionItem]: - arguments = [] - name = context.match.group("name") - domain = context.match.group("domain") or "" - - for feature in self._features.values(): - arguments += feature.complete_arguments(context, domain, name) or [] - - return arguments - - def complete_directives(self, context: CompletionContext) -> List[CompletionItem]: - self.logger.debug("Completing directives") - - items = [] - for name, directive in self.suggest_directives(context): - item = render_directive_completion(context, name, directive) - if item is None: - continue - - items.append(item) - - return items - - def completion_resolve_directive(self, item: CompletionItem) -> CompletionItem: - # We need the detail field set to the implementation's fully qualified name. - if not item.detail: - return item - - documentation = self.get_documentation(item.label, item.detail) - if not documentation: - return item - - description = documentation.get("description", "") - is_markdown = documentation.get("is_markdown", False) - kind = MarkupKind.Markdown if is_markdown else MarkupKind.PlainText - - item.documentation = MarkupContent(kind=kind, value=description) - return item - - def complete_options(self, context: CompletionContext) -> List[CompletionItem]: - surrounding_directive = self._get_surrounding_directive(context) - if not surrounding_directive: - return [] - - name = surrounding_directive.group("name") - domain = surrounding_directive.group("domain") - impl = self.get_implementation(name, domain) - if impl is None: - return [] - - items = [] - - for option in self.suggest_options(context, name, domain): - item = render_directive_option_completion(context, option, name, impl) - if item is None: - continue - - items.append(item) - - return items - - def completion_resolve_option(self, item: CompletionItem) -> CompletionItem: - # We need the detail field set to the implementation's fully qualified name. - if not item.detail or not item.data: - return item - - directive, option = item.detail.split(":") - name = typing.cast(Dict, item.data).get("for_directive", "") - - documentation = self.get_documentation(name, directive) - if not documentation: - return item - - description = documentation.get("options", {}).get(option, None) - if not description: - return item - - source = documentation.get("source", "") - license = documentation.get("license", "") - - if source: - description += f"\n\n[Source]({source})" - - if license: - description += f"\n\n[License]({license})" - - kind = MarkupKind.PlainText - if documentation.get("is_markdown", False): - kind = MarkupKind.Markdown - - item.documentation = MarkupContent(kind=kind, value=description) - return item - - def definition(self, context: DefinitionContext) -> List[Location]: - directive = context.match.group("name") - domain = context.match.group("domain") - argument = context.match.group("argument") - - if not argument: - return [] - - start = context.match.group(0).index(argument) - end = start + len(argument) - - if start <= context.position.character <= end: - return self.find_argument_definition(context, directive, domain, argument) - - return [] - - def find_argument_definition( - self, - context: DefinitionContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> List[Location]: - definitions = [] - - for feature_name, feature in self._features.items(): - try: - definitions += ( - feature.find_argument_definitions( - context, directive, domain, argument - ) - or [] - ) - except Exception: - self.logger.error( - "Unable to find definitions of '%s' for directive '%s', " - "error in feature: '%s'", - argument, - f"{domain}:{directive}" if domain else directive, - feature_name, - exc_info=True, - ) - - return definitions - - def resolve_argument_link( - self, context: DocumentLinkContext, name: str, domain: str, argument: str - ) -> Tuple[Optional[str], Optional[str]]: - for feature_name, feature in self._features.items(): - try: - target, tooltip = feature.resolve_argument_link( - context, name, domain, argument - ) - - if target: - return target, tooltip - except Exception: - self.logger.error( - "Unable to resolve argument link '%s' for directive '%s', " - "error in feature: '%s'", - argument, - f"{domain}:{name}" if domain else name, - feature_name, - exc_info=True, - ) - - return None, None - - def document_link(self, context: DocumentLinkContext) -> List[DocumentLink]: - links = [] - - for line, text in enumerate(context.doc.lines): - for match in DIRECTIVE.finditer(text): - argument = match.group("argument") - if not argument: - continue - - domain = match.group("domain") - name = match.group("name") - - target, tooltip = self.resolve_argument_link( - context, name, domain, argument - ) - if not target: - continue - - idx = match.group(0).index(argument) - start = match.start() + idx - end = start + len(argument) - - links.append( - DocumentLink( - target=target, - tooltip=tooltip if context.tooltip_support else None, - range=Range( - start=Position(line=line, character=start), - end=Position(line=line, character=end), - ), - ) - ) - - return links - - def hover(self, context: HoverContext) -> str: - if context.location not in {"rst", "docstring"}: - return "" - - name = context.match.group("name") - domain = context.match.group("domain") - - # Determine if the hover is on the .. directive:: itself, or within the argument - # Be sure to include enough chars for the length of '::'! - idx = context.position.character - context.match.start() - prefix = context.match.group(0)[:idx] - - if "::" not in prefix: - return self.hover_directive(context, name, domain) - - # TODO: Add extension points for directive arguments and options. - return "" - - def hover_directive( - self, context: HoverContext, name: str, domain: Optional[str] - ) -> str: - label = f"{domain}:{name}" if domain else name - self.logger.debug("Calculating hover for directive '%s'", label) - - directive = self.get_implementation(name, domain) - if not directive: - return "" - - try: - dotted_name = f"{directive.__module__}.{directive.__name__}" - except AttributeError: - dotted_name = f"{directive.__module__}.{directive.__class__.__name__}" - - documentation = self.get_documentation(label, dotted_name) - if not documentation: - return "" - - return documentation.get("description", "") - - def implementation(self, context: ImplementationContext) -> List[Location]: - region = context.match.group("directive") - name = context.match.group("name") - domain = context.match.group("domain") - - start = context.match.group(0).index(region) - end = start + len(region) - - if start <= context.position.character <= end: - return self.find_directive_implementation(context, name, domain) - - return [] - - def find_directive_implementation( - self, context: ImplementationContext, name: str, domain: Optional[str] - ) -> List[Location]: - impl = self.get_implementation(name, domain) - if impl is None: - return [] - - self.logger.debug( - "Getting implementation of '%s' (%s)", - f"{domain}:{name}" if domain else name, - impl, - ) - location = get_object_location(impl, self.logger) - if location is None: - return [] - - return [location] - - def _get_surrounding_directive( - self, context: CompletionContext - ) -> Optional["re.Match"]: - """Used to determine which directive we should be offering completions for. - - When suggestions should be generated this returns an :class:`python:re.Match` - object representing the directive the options are associated with. In the - case where suggestions should not be generated this will return ``None`` - - Parameters - ---------- - context: - The completion context - """ - - match = context.match - groups = match.groupdict() - indent = groups["indent"] - - self.logger.debug("Match groups: %s", groups) - - # Search backwards so that we can determine the context for our completion - linum = context.position.line - 1 - line = context.doc.lines[linum] - - while linum >= 0 and line.startswith(indent): - linum -= 1 - line = context.doc.lines[linum] - - # Only offer completions if we're within a directive's option block - directive = DIRECTIVE.match(line) - self.logger.debug("Context line: %s", line) - self.logger.debug("Context match: %s", directive) - - if not directive: - return None - - # Now that we know we're in a directive's option block, is the completion - # request coming from a valid position on the line? - option = groups["option"] - start = match.span()[0] + match.group(0).find(option) - end = start + len(option) + 1 - - if start <= context.position.character <= end: - return directive - - return None - - def get_documentation( - self, label: str, implementation: str - ) -> Optional[Dict[str, Any]]: - """Return the documentation for the given directive, if available. - - If documentation for the given ``label`` cannot be found, this function will also - look for the label under the project's :confval:`sphinx:primary_domain` followed - by the ``std`` domain. - - Parameters - ---------- - label - The name of the directive, as the user would type in an reStructuredText file. - - implementation - The full dotted name of the directive's implementation. - """ - - key = f"{label}({implementation})" - documentation = self._documentation.get(key, None) - if documentation: - return documentation - - if not isinstance(self.rst, SphinxLanguageServer) or not self.rst.app: - return None - - # Nothing found, try the primary domain - domain = self.rst.app.config.primary_domain - key = f"{domain}:{label}({implementation})" - - documentation = self._documentation.get(key, None) - if documentation: - return documentation - - # Still nothing, try the standard domain - key = f"std:{label}({implementation})" - - documentation = self._documentation.get(key, None) - if documentation: - return documentation - - return None - - -def esbonio_setup(rst: RstLanguageServer): - rst.add_feature(Directives(rst)) diff --git a/lib/esbonio/esbonio/lsp/directives/completions.py b/lib/esbonio/esbonio/lsp/directives/completions.py deleted file mode 100644 index 54b4543fe..000000000 --- a/lib/esbonio/esbonio/lsp/directives/completions.py +++ /dev/null @@ -1,353 +0,0 @@ -import re -from typing import Optional -from typing import Type - -from docutils.parsers.rst import Directive -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionItemKind -from lsprotocol.types import InsertTextFormat -from lsprotocol.types import Position -from lsprotocol.types import Range -from lsprotocol.types import TextEdit - -from esbonio.lsp import CompletionContext - -__all__ = ["render_directive_completion", "render_directive_option_completion"] - - -WORD = re.compile("[a-zA-Z]+") - - -def render_directive_completion( - context: CompletionContext, - name: str, - directive: Type[Directive], -) -> Optional[CompletionItem]: - """Render the given directive as a ``CompletionItem`` according to the current - context. - - Parameters - ---------- - context - The context in which the completion should be rendered. - - name - The name of the directive, as it appears in an rst file. - - directive - The class that implements the directive. - - Returns - ------- - Optional[CompletionItem] - The final completion item or ``None``. - If ``None`` is returned, then the given completion should be skipped. - """ - - if context.config.preferred_insert_behavior == "insert": - return _render_directive_with_insert_text(context, name, directive) - - return _render_directive_with_text_edit(context, name, directive) - - -def render_directive_option_completion( - context: CompletionContext, - name: str, - directive: str, - implementation: Type[Directive], -) -> Optional[CompletionItem]: - """Render the given directive option as a ``CompletionItem`` according to the - current context. - - Parameters - ---------- - context - The context in which the completion should be rendered. - - name - The name of the option, as it appears in an rst file. - - directive - The name of the directive, as it appears in an rst file. - - implementation - The class implementing the directive. - - Returns - ------- - Optional[CompletionItem] - The final completion item or ``None``. - If ``None`` is returned, the given completion should be skipped. - """ - - if context.config.preferred_insert_behavior == "insert": - return _render_directive_option_with_insert_text( - context, name, directive, implementation - ) - - return _render_directive_option_with_text_edit( - context, name, directive, implementation - ) - - -def _render_directive_with_insert_text( - context: CompletionContext, - name: str, - directive: Type[Directive], -) -> Optional[CompletionItem]: - """Render a ``CompletionItem`` using ``insertText`` fields. - - This implements the ``insert`` behavior for directives. - Parameters - ---------- - context - The context in which the completion is being generated. - - name - The name of the directive, as it appears in an rst file. - - directive - The class implementing the directive. - - """ - insert_text = f".. {name}::" - user_text = context.match.group(0).strip() - - # Since we can't replace any existing text, it only makes sense - # to offer completions that ailgn with what the user has already written. - if not insert_text.startswith(user_text): - return None - - # Except that's not entirely true... to quote the LSP spec. (emphasis added) - # - # > in the model the client should filter against what the user has already typed - # > **using the word boundary rules of the language** (e.g. resolving the word - # > under the cursor position). The reason for this mode is that it makes it - # > extremely easy for a server to implement a basic completion list and get it - # > filtered on the client. - # - # So in other words... if the cursor is inside a word, that entire word will be - # replaced with what we have in `insert_text` so we need to be able to do something - # like - # .. -> image:: - # .. im -> image:: - # - # .. -> code-block:: - # .. cod -> code-block:: - # .. code-bl -> block:: - # - # .. -> c:function:: - # .. c -> c:function:: - # .. c: -> function:: - # .. c:fun -> function:: - # - # And since the client is free to interpret this how it likes, it's unlikely we'll - # be able to get this right in all cases for all clients. So for now this is going - # to target Kate's interpretation since it currently does not support ``text_edit`` - # and it was the editor that prompted this to be implemented in the first place. - # - # See: https://github.com/swyddfa/esbonio/issues/471 - - # If the existing text ends with a delimiter, then we should simply remove the - # entire prefix - if user_text.endswith((":", "-", " ")): - start_index = len(user_text) - - # Look for groups of word chars, replace text until the start of the final group - else: - start_indices = [m.start() for m in WORD.finditer(user_text)] or [ - len(user_text) - ] - start_index = max(start_indices) - - item = _render_directive_common(name, directive) - item.insert_text = insert_text[start_index:] - return item - - -def _render_directive_with_text_edit( - context: CompletionContext, - name: str, - directive: Type[Directive], -) -> Optional[CompletionItem]: - """Render a directive's ``CompletionItem`` using the ``textEdit`` field. - - This implements the ``replace`` insert behavior for directives. - - Parameters - ---------- - context - The context in which the completion is being generated. - - name - The name of the directive, as it appears in an rst file. - - directive - The class implementing the directive. - - """ - match = context.match - - # Calculate the range of text the CompletionItems should edit. - # If there is an existing argument to the directive, we should leave it untouched - # otherwise, edit the whole line to insert any required arguments. - start = match.span()[0] + match.group(0).find(".") - include_argument = context.snippet_support - end = match.span()[1] - - if match.group("argument"): - include_argument = False - end = match.span()[0] + match.group(0).find("::") + 2 - - range_ = Range( - start=Position(line=context.position.line, character=start), - end=Position(line=context.position.line, character=end), - ) - - # TODO: Give better names to arguments based on what they represent. - if include_argument: - insert_format = InsertTextFormat.Snippet - nargs = getattr(directive, "required_arguments", 0) - args = " " + " ".join("${{{0}:arg{0}}}".format(i) for i in range(1, nargs + 1)) - else: - args = "" - insert_format = InsertTextFormat.PlainText - - insert_text = f".. {name}::{args}" - - item = _render_directive_common(name, directive) - item.filter_text = insert_text - item.text_edit = TextEdit(range=range_, new_text=insert_text) - item.insert_text_format = insert_format - - return item - - -def _render_directive_common( - name: str, - directive: Type[Directive], -) -> CompletionItem: - """Render the common fields of a directive's completion item.""" - - try: - dotted_name = f"{directive.__module__}.{directive.__name__}" - except AttributeError: - dotted_name = f"{directive.__module__}.{directive.__class__.__name__}" - - return CompletionItem( - label=name, - detail=dotted_name, - kind=CompletionItemKind.Class, - data={"completion_type": "directive"}, - ) - - -def _render_directive_option_with_insert_text( - context: CompletionContext, - name: str, - directive: str, - implementation: Type[Directive], -) -> Optional[CompletionItem]: - """Render a directive option's ``CompletionItem`` using the ``insertText`` field. - - This implements the ``insert`` insert behavior for directive options. - - Parameters - ---------- - context - The context in which the completion is being generated. - - name - The name of the directive option, as it appears in an rst file. - - directive - The name of the directive, as it appears in an rst file. - - implementation - The class implementing the directive. - - """ - - insert_text = f":{name}:" - user_text = context.match.group(0).strip() - - if not insert_text.startswith(user_text): - return None - - if user_text.endswith((":", "-", " ")): - start_index = len(user_text) - - else: - start_indices = [m.start() for m in WORD.finditer(user_text)] or [ - len(user_text) - ] - start_index = max(start_indices) - - item = _render_directive_option_common(name, directive, implementation) - item.insert_text = insert_text[start_index:] - return item - - -def _render_directive_option_with_text_edit( - context: CompletionContext, - name: str, - directive: str, - implementation: Type[Directive], -) -> CompletionItem: - """Render a directive option's ``CompletionItem`` using the``textEdit`` field. - - This implements the ``replace`` insert behavior for directive options. - - Parameters - ---------- - context - The context in which the completion is being generated. - - name - The name of the directive option, as it appears in an rst file. - - directive - The name of the directive, as it appears in an rst file. - - implementation - The class implementing the directive. - - """ - - match = context.match - groups = match.groupdict() - - option = groups["option"] - start = match.span()[0] + match.group(0).find(option) - end = start + len(option) - - range_ = Range( - start=Position(line=context.position.line, character=start), - end=Position(line=context.position.line, character=end), - ) - - insert_text = f":{name}:" - - item = _render_directive_option_common(name, directive, implementation) - item.filter_text = insert_text - item.text_edit = TextEdit(range=range_, new_text=insert_text) - - return item - - -def _render_directive_option_common( - name: str, directive: str, impl: Type[Directive] -) -> CompletionItem: - """Render the common fields of a directive option's completion item.""" - - try: - impl_name = f"{impl.__module__}.{impl.__name__}" - except AttributeError: - impl_name = f"{impl.__module__}.{impl.__class__.__name__}" - - return CompletionItem( - label=name, - detail=f"{impl_name}:{name}", - kind=CompletionItemKind.Field, - data={"completion_type": "directive_option", "for_directive": directive}, - ) diff --git a/lib/esbonio/esbonio/lsp/log.py b/lib/esbonio/esbonio/lsp/log.py deleted file mode 100644 index ef7cad2c8..000000000 --- a/lib/esbonio/esbonio/lsp/log.py +++ /dev/null @@ -1,180 +0,0 @@ -import logging -import pathlib -import traceback -import typing -from typing import List -from typing import Tuple - -import pygls.uris as uri -from lsprotocol.types import Diagnostic -from lsprotocol.types import DiagnosticSeverity -from lsprotocol.types import DiagnosticTag -from lsprotocol.types import Position -from lsprotocol.types import Range - -if typing.TYPE_CHECKING: - from .rst import RstLanguageServer - from .rst.config import ServerConfig - - -LOG_NAMESPACE = "esbonio.lsp" -LOG_LEVELS = { - "debug": logging.DEBUG, - "error": logging.ERROR, - "info": logging.INFO, -} - - -class LogFilter(logging.Filter): - """A log filter that accepts message from any of the listed logger names.""" - - def __init__(self, names): - self.names = names - - def filter(self, record): - return any(record.name == name for name in self.names) - - -class MemoryHandler(logging.Handler): - """A logging handler that caches messages in memory.""" - - def __init__(self): - super().__init__() - self.records: List[logging.LogRecord] = [] - - def emit(self, record: logging.LogRecord) -> None: - self.records.append(record) - - -class LspHandler(logging.Handler): - """A logging handler that will send log records to an LSP client.""" - - def __init__( - self, server: "RstLanguageServer", show_deprecation_warnings: bool = False - ): - super().__init__() - self.server = server - self.show_deprecation_warnings = show_deprecation_warnings - - def get_warning_path(self, warning: str) -> Tuple[str, List[str]]: - """Determine the filepath that the warning was emitted from.""" - - path, *parts = warning.split(":") - - # On windows the rest of the path will be in the first element of parts. - if pathlib.Path(warning).drive: - path += f":{parts.pop(0)}" - - return path, parts - - def handle_warning(self, record: logging.LogRecord): - """Publish warnings to the client as diagnostics.""" - - if not isinstance(record.args, tuple): - self.server.logger.debug( - "Unable to handle warning, expected tuple got: %s", record.args - ) - return - - # The way warnings are logged is different in Python 3.11+ - if len(record.args) == 0: - argument = record.msg - else: - argument = record.args[0] # type: ignore - - if not isinstance(argument, str): - self.server.logger.debug( - "Unable to handle warning, expected string got: %s", argument - ) - return - - warning, *_ = argument.split("\n") - path, (linenum, category, *msg) = self.get_warning_path(warning) - - category = category.strip() - message = ":".join(msg).strip() - - try: - line = int(linenum) - except ValueError: - line = 1 - self.server.logger.debug( - "Unable to parse line number: '%s'\n%s", linenum, traceback.format_exc() - ) - - tags = [] - if category == "DeprecationWarning": - tags.append(DiagnosticTag.Deprecated) - - diagnostic = Diagnostic( - range=Range( - start=Position(line=line - 1, character=0), - end=Position(line=line, character=0), - ), - message=message, - severity=DiagnosticSeverity.Warning, - tags=tags, - ) - - self.server.add_diagnostics("esbonio", uri.from_fs_path(path), diagnostic) - self.server.sync_diagnostics() - - def emit(self, record: logging.LogRecord) -> None: - """Sends the record to the client.""" - - # To avoid infinite recursions, it's simpler to just ignore all log records - # coming from pygls... - if "pygls" in record.name: - return - - if record.name == "py.warnings": - if not self.show_deprecation_warnings: - return - - self.handle_warning(record) - - log = self.format(record).strip() - self.server.show_message_log(log) - - -def setup_logging(server: "RstLanguageServer", config: "ServerConfig"): - """Setup logging to route log messages to the language client as - ``window/logMessage`` messages. - - Parameters - ---------- - server - The server to use to send messages - - config - The configuration to use - """ - - level = LOG_LEVELS[config.log_level] - - warnlog = logging.getLogger("py.warnings") - logger = logging.getLogger(LOG_NAMESPACE) - logger.setLevel(level) - - lsp_handler = LspHandler(server, config.show_deprecation_warnings) - lsp_handler.setLevel(level) - - if len(config.log_filter) > 0: - lsp_handler.addFilter(LogFilter(config.log_filter)) - - formatter = logging.Formatter("[%(name)s] %(message)s") - lsp_handler.setFormatter(formatter) - - # Look to see if there are any cached messages we should forward to the client. - for handler in logger.handlers: - if not isinstance(handler, MemoryHandler): - continue - - for record in handler.records: - if logger.isEnabledFor(record.levelno): - lsp_handler.emit(record) - - logger.removeHandler(handler) - - logger.addHandler(lsp_handler) - warnlog.addHandler(lsp_handler) diff --git a/lib/esbonio/esbonio/lsp/py.typed b/lib/esbonio/esbonio/lsp/py.typed deleted file mode 100644 index 8280d08cd..000000000 --- a/lib/esbonio/esbonio/lsp/py.typed +++ /dev/null @@ -1 +0,0 @@ -# esbonio.lsp: marker file for PEP 561 diff --git a/lib/esbonio/esbonio/lsp/roles/__init__.py b/lib/esbonio/esbonio/lsp/roles/__init__.py deleted file mode 100644 index 6efd05033..000000000 --- a/lib/esbonio/esbonio/lsp/roles/__init__.py +++ /dev/null @@ -1,867 +0,0 @@ -"""Role support.""" -import typing -import warnings -from typing import Any -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional -from typing import Protocol -from typing import Tuple - -from lsprotocol.types import CompletionItem -from lsprotocol.types import DocumentLink -from lsprotocol.types import Location -from lsprotocol.types import MarkupContent -from lsprotocol.types import MarkupKind -from lsprotocol.types import Position -from lsprotocol.types import Range -from lsprotocol.types import TextEdit - -from esbonio.lsp.rst import CompletionContext -from esbonio.lsp.rst import DefinitionContext -from esbonio.lsp.rst import DocumentLinkContext -from esbonio.lsp.rst import HoverContext -from esbonio.lsp.rst import ImplementationContext -from esbonio.lsp.rst import LanguageFeature -from esbonio.lsp.rst import RstLanguageServer -from esbonio.lsp.sphinx import SphinxLanguageServer -from esbonio.lsp.util.inspect import get_object_location -from esbonio.lsp.util.patterns import DEFAULT_ROLE -from esbonio.lsp.util.patterns import DIRECTIVE -from esbonio.lsp.util.patterns import ROLE - -from .completions import render_role_completion - - -class RoleLanguageFeature: - """Base class for role language features.""" - - def complete_targets( - self, context: CompletionContext, name: str, domain: str - ) -> List[CompletionItem]: - """Return a list of completion items representing valid targets for the given - role. - - Parameters - ---------- - context - The completion context - - name - The name of the role to generate completion suggestions for. - - domain - The name of the domain the role is a member of - """ - return [] - - def find_target_definitions( - self, context: DefinitionContext, name: str, domain: str, label: str - ) -> List[Location]: - """Return a list of locations representing the definition of the given role - target. - - Parameters - ---------- - doc: - The document containing the match - match: - The match object that triggered the definition request - name: - The name of the role - domain: - The domain the role is part of, if applicable. - """ - return [] - - def get_implementation(self, role: str, domain: str) -> Optional[Any]: - """Return the implementation for the given role name. - - Parameters - ---------- - role - The name of the role - - domain - The domain the role belongs to, if any - """ - return self.index_roles().get(role, None) - - def index_roles(self) -> Dict[str, Any]: - """Return all known roles.""" - return dict() - - def resolve_target_link( - self, context: DocumentLinkContext, name: str, domain: Optional[str], label: str - ) -> Tuple[Optional[str], Optional[str]]: - """Return a link corresponding to the given target. - - Parameters - ---------- - context - The document link context - - domain - The name (if applicable) of the domain the role is a member of - - name - The name of the role to generate completion suggestions for. - - label - The label of the target to provide the link for - """ - return None, None - - def suggest_roles(self, context: CompletionContext) -> Iterable[Tuple[str, Any]]: - """Suggest roles that may be used, given a completion context.""" - return self.index_roles().items() - - -class TargetDefinition(Protocol): - """A definition provider for role targets. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0``, use a subclass of - :class:`~esbonio.lsp.roles.RoleLanguageFeature` instead. - """ - - def find_definitions( - self, context: DefinitionContext, name: str, domain: Optional[str] - ) -> List[Location]: - """Return a list of locations representing the definition of the given role - target. - - Parameters - ---------- - doc: - The document containing the match - match: - The match object that triggered the definition request - name: - The name of the role - domain: - The domain the role is part of, if applicable. - """ - - -class TargetCompletion(Protocol): - """A completion provider for role targets. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0``, use a subclass of - :class:`~esbonio.lsp.roles.RoleLanguageFeature` instead. - """ - - def complete_targets( - self, context: CompletionContext, name: str, domain: Optional[str] - ) -> List[CompletionItem]: - """Return a list of completion items representing valid targets for the given - role. - - Parameters - ---------- - context: - The completion context - domain: - The name of the domain the role is a member of - name: - The name of the role to generate completion suggestions for. - """ - - -class TargetLink(Protocol): - """A document link provider for role targets. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0``, use a subclass of - :class:`~esbonio.lsp.roles.RoleLanguageFeature` instead. - """ - - def resolve_link( - self, context: DocumentLinkContext, name: str, domain: Optional[str], label: str - ) -> Tuple[Optional[str], Optional[str]]: - """Return a link corresponding to the given target. - - Parameters - ---------- - context - The document link context - - domain - The name (if applicable) of the domain the role is a member of - - name - The name of the role to generate completion suggestions for. - - label - The label of the target to provide the link for - """ - - -class Roles(LanguageFeature): - """Role support for the language server.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self._documentation: Dict[str, Dict[str, str]] = {} - """Cache for documentation.""" - - self._features: Dict[str, RoleLanguageFeature] = {} - """Collection of registered features""" - - def add_feature(self, feature: RoleLanguageFeature): - """Register a role language feature - - Parameters - ---------- - feature - The role language feature - """ - key = f"{feature.__module__}.{feature.__class__.__name__}" - - # Create a unique key for this instance. - if key in self._features: - key += f".{len([k for k in self._features.keys() if k.startswith(key)])}" - - self._features[key] = feature - - def add_target_definition_provider(self, provider: TargetDefinition) -> None: - """Register a :class:`~esbonio.lsp.roles.TargetDefinition` provider. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0`` use - :meth:`~esbonio.lsp.roles.Roles.add_feature` with a - :class:`~esbonio.lsp.roles.RoleLanguageFeature` subclass instead. - - Parameters - ---------- - provider - The provider to register - """ - - warnings.warn( - "TargetDefinition providers are deprecated in favour of " - "RoleLanguageFeatures, this method will be removed in v1.0", - DeprecationWarning, - stacklevel=2, - ) - - name = provider.__class__.__name__ - key = f"{provider.__module__}.{name}.definition" - - def find_target_definitions(self, context, name, domain, label): - return provider.find_definitions(context, name, domain) - - feature = type( - f"{name}TargetDefinitionProvider", - (RoleLanguageFeature,), - {"find_target_definitions": find_target_definitions}, - )() - - self._features[key] = feature - - def add_target_link_provider(self, provider: TargetLink) -> None: - """Register a :class:`~esbonio.lsp.roles.TargetLink` provider. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0`` use - :meth:`~esbonio.lsp.roles.Roles.add_feature` with a - :class:`~esbonio.lsp.roles.RoleLanguageFeature` subclass instead. - - Parameters - ---------- - provider - The provider to register - """ - - warnings.warn( - "TargetLink providers are deprecated in favour of " - "RoleLanguageFeatures, this method will be removed in v1.0", - DeprecationWarning, - stacklevel=2, - ) - - name = provider.__class__.__name__ - key = f"{provider.__module__}.{name}.link" - - feature = type( - f"{name}TargetLinkProvider", - (RoleLanguageFeature,), - {"resolve_target_link": provider.resolve_link}, - )() - - self._features[key] = feature - - def add_target_completion_provider(self, provider: TargetCompletion) -> None: - """Register a :class:`~esbonio.lsp.roles.TargetCompletion` provider. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0`` use - :meth:`~esbonio.lsp.roles.Roles.add_feature` with a - :class:`~esbonio.lsp.roles.RoleLanguageFeature` subclass instead. - - Parameters - ---------- - provider - The provider to register - """ - - warnings.warn( - "TargetCompletion providers are deprecated in favour of " - "RoleLanguageFeatures, this method will be removed in v1.0", - DeprecationWarning, - stacklevel=2, - ) - - name = provider.__class__.__name__ - key = f"{provider.__module__}.{name}.completion" - - feature = type( - f"{name}TargetCompletionProvider", - (RoleLanguageFeature,), - {"complete_targets": provider.complete_targets}, - )() - - self._features[key] = feature - - def add_documentation(self, documentation: Dict[str, Dict[str, Any]]) -> None: - """Register role documentation. - - ``documentation`` should be a dictionary of the form :: - - documentation = { - "raw(docutils.parsers.rst.roles.raw_role)": { - "is_markdown": true, - "license": "https://...", - "source": "https://...", - "description": [ - "# :raw:", - "The raw role is used for...", - ... - ] - } - } - - where the key is of the form `name(dotted_name)`. There are cases where a role's - implementation is not sufficient to uniquely identify it as multiple roles can - be provided by a single class. - - This means the key has to be a combination of the ``name`` the user writes in - an reStructuredText document and ``dotted_name`` is the fully qualified name of - the role's implementation. - - .. note:: - - If there is a clash with an existing key, the existing value will be - overwritten with the new value. - - The values in this dictionary are themselves dictionaries with the following - fields. - - ``description`` - A list of strings for the role's usage. - - ``is_markdown`` - A boolean flag used to indicate whether the ``description`` is written in - plain text or markdown. - - ``source`` - The url to the documentation's source. - - ``license`` - The url to the documentation's license. - - Parameters - ---------- - documentation: - The documentation to register. - """ - - for key, doc in documentation.items(): - description = doc.get("description", []) - if not description: - continue - - source = doc.get("source", "") - if source: - description.append(f"\n[Source]({source})") - - license = doc.get("license", "") - if license: - description.append(f"\n[License]({license})") - - doc["description"] = "\n".join(description) - self._documentation[key] = doc - - completion_triggers = [ROLE, DEFAULT_ROLE] - definition_triggers = [ROLE] - hover_triggers = [ROLE] - implementation_triggers = [ROLE] - - def definition(self, context: DefinitionContext) -> List[Location]: - domain = context.match.group("domain") or "" - name = context.match.group("name") - label = context.match.group("label") - - # Be sure to only match complete roles - if not label or not context.match.group(0).endswith("`"): - return [] - - return self.find_target_definitions(context, name, domain, label) - - def find_target_definitions( - self, context: DefinitionContext, name: str, domain: str, label: str - ) -> List[Location]: - definitions = [] - - for feature_name, feature in self._features.items(): - try: - definitions += feature.find_target_definitions( - context, name, domain, label - ) - except Exception: - self.logger.error( - "Unable to find definitions of '%s' for role ':%s:', " - "error in feature: '%s'", - label, - f"{domain}:{name}" if domain else name, - feature_name, - exc_info=True, - ) - - return definitions - - def document_link(self, context: DocumentLinkContext) -> List[DocumentLink]: - links = [] - - for line, text in enumerate(context.doc.lines): - for match in ROLE.finditer(text): - label = match.group("label") - - # Be sure to only match complete roles - if not label or not match.group(0).endswith("`"): - continue - - domain = match.group("domain") - name = match.group("name") - - target, tooltip = self.resolve_target_link(context, name, domain, label) - if not target: - continue - - idx = match.group(0).index(label) - start = match.start() + idx - end = start + len(label) - - link = DocumentLink( - target=target, - tooltip=tooltip if context.tooltip_support else None, - range=Range( - start=Position(line=line, character=start), - end=Position(line=line, character=end), - ), - ) - - links.append(link) - - return links - - def resolve_target_link( - self, context: DocumentLinkContext, name: str, domain: str, label: str - ) -> Tuple[Optional[str], Optional[str]]: - """Resolve a given document link.""" - - for feature_name, feature in self._features.items(): - try: - target, tooltip = feature.resolve_target_link( - context, name, domain, label - ) - - if target: - return target, tooltip - except Exception: - self.logger.error( - "Unable to resolve target link '%s' for role ':%s:', " - "error in feature: '%s'", - label, - f"{domain}:{name}" if domain else name, - feature_name, - exc_info=True, - ) - - return None, None - - def complete(self, context: CompletionContext) -> List[CompletionItem]: - """Generate completion suggestions relevant to the current context. - - This function is a little intense, but its sole purpose is to determine the - context in which the completion request is being made and either return - nothing, or the results of :meth:`~esbonio.lsp.roles.Roles.complete_roles` or - :meth:`esbonio.lsp.roles.Roles.complete_targets` whichever is appropriate. - - Parameters - ---------- - context: - The context of the completion request. - """ - - # Do not suggest completions within the middle of Python code. - if context.location == "py": - return [] - - groups = context.match.groupdict() - target = groups["target"] - - # All text matched by the regex - text = context.match.group(0) - start, end = context.match.span() - - if target: - target_index = start + text.find(target) - - # Only trigger target completions if the request was made from within - # the target part of the role. - if target_index <= context.position.character <= end: - return self.complete_targets(context) - - # If there's no indent, then this can only be a role definition - indent = context.match.group(1) - if indent == "": - return self.complete_roles(context) - - # Otherwise, search backwards until we find a blank line or an unindent - # so that we can determine the appropriate context. - linum = context.position.line - 1 - - try: - line = context.doc.lines[linum] - except IndexError: - return self.complete_roles(context) - - while linum >= 0 and line.startswith(indent): - linum -= 1 - line = context.doc.lines[linum] - - # Unless we are within a directive's options block, we should offer role - # suggestions - if DIRECTIVE.match(line): - return [] - - return self.complete_roles(context) - - def completion_resolve(self, item: CompletionItem) -> CompletionItem: - # We need extra info to know who to call - if not item.data: - return item - - data = typing.cast(Dict, item.data) - ctype = data.get("completion_type", "") - - if ctype == "role": - return self.completion_resolve_role(item) - - return item - - def suggest_roles(self, context: CompletionContext) -> Iterable[Tuple[str, Any]]: - """Suggest roles that may be used, given a completion context. - - Parameters - ---------- - context - The completion context - """ - for name, feature in self._features.items(): - try: - yield from feature.suggest_roles(context) - except Exception: - self.logger.error( - "Unable to suggest roles, error in feature: '%s'", - name, - exc_info=True, - ) - - def complete_roles(self, context: CompletionContext) -> List[CompletionItem]: - items = [] - - for name, role in self.suggest_roles(context): - item = render_role_completion(context, name, role) - if item is None: - continue - - items.append(item) - - return items - - def completion_resolve_role(self, item: CompletionItem) -> CompletionItem: - # We need the detail field set to the role implementation's fully qualified name - if not item.detail: - return item - - documentation = self.get_documentation(item.label, item.detail) - if not documentation: - return item - - description = documentation.get("description", "") - is_markdown = documentation.get("is_markdown", False) - kind = MarkupKind.Markdown if is_markdown else MarkupKind.PlainText - - item.documentation = MarkupContent(kind=kind, value=description) - return item - - def suggest_targets( - self, context: CompletionContext, name: str, domain: str - ) -> List[CompletionItem]: - targets = [] - - for feature_name, feature in self._features.items(): - try: - targets += feature.complete_targets(context, name, domain) - except Exception: - self.logger.error( - "Unable to suggest targets for role ':%s:', error in feature: '%s'", - f"{domain}:{name}" if domain else name, - feature_name, - exc_info=True, - ) - - return targets - - def complete_targets(self, context: CompletionContext) -> List[CompletionItem]: - """Generate the list of role target completion suggestions.""" - - groups = context.match.groupdict() - - # Handle the default role case. - if "role" not in groups: - domain, name = self.rst.get_default_role() - if not name: - return [] - else: - name = groups["name"] - domain = groups["domain"] - - domain = domain or "" - name = name or "" - - # Only generate suggestions for "aliased" targets if the request comes from - # within the <> chars. - if groups["alias"]: - text = context.match.group(0) - start = context.match.span()[0] + text.find(groups["alias"]) - end = start + len(groups["alias"]) - - if start <= context.position.character <= end: - return [] - - targets = [] - - startchar = "<" if "<" in groups["target"] else "`" - endchars = ">`" if "<" in groups["target"] else "`" - - start, end = context.match.span() - start += context.match.group(0).index(startchar) + 1 - range_ = Range( - start=Position(line=context.position.line, character=start), - end=Position(line=context.position.line, character=end), - ) - prefix = context.match.group(0)[start:] - modifier = groups["modifier"] or "" - - for candidate in self.suggest_targets(context, name, domain): - # Don't interfere with items that already carry a `text_edit`, allowing - # some providers (like filepaths) to do something special. - if not candidate.text_edit: - new_text = candidate.insert_text or candidate.label - - # This is rather annoying, but `filter_text` needs to start with - # the text we are going to replace, otherwise VSCode won't show our - # suggestions! - candidate.filter_text = f"{prefix}{new_text}" - - candidate.text_edit = TextEdit( - range=range_, new_text=f"{modifier}{new_text}" - ) - candidate.insert_text = None - - if not candidate.text_edit.new_text.endswith(endchars): - candidate.text_edit.new_text += endchars - - targets.append(candidate) - - return targets - - def hover(self, context: HoverContext) -> str: - if context.location not in {"rst", "docstring"}: - return "" - - name = context.match.group("name") - domain = context.match.group("domain") - - # Determine if the hover is on the :role: itself, or within the `target`. - idx = context.position.character - context.match.start() - prefix = context.match.group(0)[:idx] - - if "`" in prefix: - return self.hover_target(context, name, domain) - - return self.hover_role(context, name, domain) - - def hover_role(self, context: HoverContext, name: str, domain: str) -> str: - label = f"{domain}:{name}" if domain else name - role = self.get_implementation(name, domain) - if not role: - return "" - - try: - dotted_name = f"{role.__module__}.{role.__name__}" - except AttributeError: - dotted_name = f"{role.__module__}.{role.__class__.__name__}" - - documentation = self.get_documentation(label, dotted_name) - if not documentation: - return "" - - return documentation.get("description", "") - - def hover_target( - self, context: HoverContext, name: str, domain: Optional[str] - ) -> str: - # TODO: Add extension point for providers to contribute hovers for a target. - return "" - - def get_roles(self) -> Dict[str, Any]: - """Return a dictionary of all known roles.""" - - roles = {} - - for name, feature in self._features.items(): - self.logger.debug("calling '%s'", name) - try: - roles.update(feature.index_roles()) - except Exception: - self.logger.error( - "Unable to index roles, error in feature '%s'", name, exc_info=True - ) - - return roles - - def get_implementation(self, role: str, domain: str) -> Optional[Any]: - """Return the implementation of a role given its name - - Parameters - ---------- - role - The name of the role. - - domain - The domain of the role, if applicable. - """ - - if domain: - name = f"{domain}:{role}" - else: - name = role - - for feature_name, feature in self._features.items(): - try: - impl = feature.get_implementation(role, domain) - if impl is not None: - return impl - except Exception: - self.logger.error( - "Unable to get implementation for ':%s:', error in feature: '%s'\n%s", - name, - feature_name, - exc_info=True, - ) - - self.logger.debug("Unable to get implementation for ':%s:', unknown role", name) - return None - - def implementation(self, context: ImplementationContext) -> List[Location]: - region = context.match.group("role") - name = context.match.group("name") - domain = context.match.group("domain") - - start = context.match.start() + context.match.group(0).index(region) - end = start + len(region) - - self.logger.debug("%s, %s, %s", region, name, domain) - self.logger.debug("%s, %s", start, end) - - if start <= context.position.character <= end: - return self.find_role_implementation(context, name, domain) - - return [] - - def find_role_implementation( - self, context: ImplementationContext, name: str, domain: str - ) -> List[Location]: - impl = self.get_implementation(name, domain) - if impl is None: - return [] - - location = get_object_location(impl, self.logger) - if location is not None: - return [location] - - # Some roles are implemented as instances of some class - location = get_object_location(impl.__class__, self.logger) - if location is not None: - return [location] - - return [] - - def get_documentation( - self, label: str, implementation: str - ) -> Optional[Dict[str, Any]]: - """Return the documentation for the given role, if available. - - If documentation for the given ``label`` cannot be found, this function will also - look for the label under the project's :confval:`sphinx:primary_domain` followed - by the ``std`` domain. - - Parameters - ---------- - label: - The name of the role, as the user would type in an reStructuredText file. - implementation: - The full dotted name of the role's implementation. - """ - - key = f"{label}({implementation})" - documentation = self._documentation.get(key, None) - if documentation: - return documentation - - if not isinstance(self.rst, SphinxLanguageServer) or not self.rst.app: - return None - - # Nothing found, try the primary domain - domain = self.rst.app.config.primary_domain - key = f"{domain}:{label}({implementation})" - - documentation = self._documentation.get(key, None) - if documentation: - return documentation - - # Still nothing, try the standard domain - key = f"std:{label}({implementation})" - - documentation = self._documentation.get(key, None) - if documentation: - return documentation - - return None - - -def esbonio_setup(rst: RstLanguageServer): - rst.add_feature(Roles(rst)) diff --git a/lib/esbonio/esbonio/lsp/roles/completions.py b/lib/esbonio/esbonio/lsp/roles/completions.py deleted file mode 100644 index 51178aa62..000000000 --- a/lib/esbonio/esbonio/lsp/roles/completions.py +++ /dev/null @@ -1,147 +0,0 @@ -import re -from typing import Any -from typing import Optional - -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionItemKind -from lsprotocol.types import Position -from lsprotocol.types import Range -from lsprotocol.types import TextEdit - -from esbonio.lsp import CompletionContext - -__all__ = ["render_role_completion"] - - -WORD = re.compile("[a-zA-Z]+") - - -def render_role_completion( - context: CompletionContext, - name: str, - role: Any, -) -> Optional[CompletionItem]: - """Render the given role as a ``CompletionItem`` according to the current - context. - - Parameters - ---------- - context - The context in which the completion should be rendered. - - name - The name of the role, as it appears in an rst file. - - role - The implementation of the role. - - Returns - ------- - Optional[CompletionItem] - The final completion item or ``None``. - If ``None``, then the given completion should be skipped. - """ - - if context.config.preferred_insert_behavior == "insert": - return _render_role_with_insert_text(context, name, role) - - return _render_role_with_text_edit(context, name, role) - - -def _render_role_with_insert_text(context, name, role): - """Render a role's ``CompletionItem`` using the ``insertText`` field. - - This implements the ``insert`` insert behavior for roles. - - Parameters - ---------- - context - The context in which the completion is being generated. - - name - The name of the role, as it appears in an rst file. - - role - The implementation of the role. - """ - - insert_text = f":{name}:" - user_text = context.match.group(0).strip() - - if not insert_text.startswith(user_text): - return None - - # If the existing text ends with a delimiter, then we should simply remove the - # entire prefix - if user_text.endswith((":", "-", " ")): - start_index = len(user_text) - - # Look for groups of word chars, replace text until the start of the final group - else: - start_indices = [m.start() for m in WORD.finditer(user_text)] or [ - len(user_text) - ] - start_index = max(start_indices) - - item = _render_role_common(name, role) - item.insert_text = insert_text[start_index:] - return item - - -def _render_role_with_text_edit( - context: CompletionContext, name: str, role: Any -) -> Optional[CompletionItem]: - """Render a role's ``CompletionItem`` using the ``textEdit`` field. - - This implements the ``replace`` insert behavior for roles. - - Parameters - ---------- - context - The context in which the completion is being generated. - - name - The name of the role, as it appears in an rst file. - - role - The implementation of the role. - """ - match = context.match - groups = match.groupdict() - domain = groups["domain"] or "" - - if not name.startswith(domain): - return None - - # Insert text starting from the starting ':' character of the role. - start = match.span()[0] + match.group(0).find(":") - end = start + len(groups["role"]) - - range_ = Range( - start=Position(line=context.position.line, character=start), - end=Position(line=context.position.line, character=end), - ) - - insert_text = f":{name}:" - - item = _render_role_common(name, role) - item.filter_text = insert_text - item.text_edit = TextEdit(range=range_, new_text=insert_text) - - return item - - -def _render_role_common(name: str, role: Any) -> CompletionItem: - """Render the common fields of a role's completion item.""" - - try: - dotted_name = f"{role.__module__}.{role.__name__}" - except AttributeError: - dotted_name = f"{role.__module__}.{role.__class__.__name__}" - - return CompletionItem( - label=name, - kind=CompletionItemKind.Function, - detail=f"{dotted_name}", - data={"completion_type": "role"}, - ) diff --git a/lib/esbonio/esbonio/lsp/rst/__init__.py b/lib/esbonio/esbonio/lsp/rst/__init__.py deleted file mode 100644 index aa02a288a..000000000 --- a/lib/esbonio/esbonio/lsp/rst/__init__.py +++ /dev/null @@ -1,879 +0,0 @@ -import collections -import inspect -import logging -import pathlib -import re -import traceback -import typing -import warnings -from typing import Any -from typing import Callable -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple -from typing import Type -from typing import TypeVar - -import pygls.uris as Uri -from docutils.parsers.rst import Directive -from lsprotocol.converters import get_converter -from lsprotocol.types import ClientCapabilities -from lsprotocol.types import CodeAction -from lsprotocol.types import CodeActionParams -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionItemTag -from lsprotocol.types import DeleteFilesParams -from lsprotocol.types import Diagnostic -from lsprotocol.types import DidSaveTextDocumentParams -from lsprotocol.types import DocumentLink -from lsprotocol.types import InitializedParams -from lsprotocol.types import InitializeParams -from lsprotocol.types import Location -from lsprotocol.types import MarkupKind -from lsprotocol.types import Position -from pygls import IS_WIN -from pygls.capabilities import get_capability -from pygls.server import LanguageServer -from pygls.workspace import Document - -from esbonio.cli import setup_cli -from esbonio.lsp.log import setup_logging - -from .config import InitializationOptions -from .config import ServerCompletionConfig -from .io import read_initial_doctree - -converter = get_converter() -LF = TypeVar("LF", bound="LanguageFeature") -TRIPLE_QUOTE = re.compile("(\"\"\"|''')") -"""A regular expression matching the triple quotes used to delimit python docstrings.""" - -# fmt: off -# Order matters! -DEFAULT_MODULES = [ - "esbonio.lsp.directives", # Generic directive support - "esbonio.lsp.roles", # Generic roles support - "esbonio.lsp.rst.directives", # Specialised support for docutils directives - "esbonio.lsp.rst.roles", # Specialised support for docutils roles -] -"""The modules to load in the default configuration of the server.""" -# fmt: on - - -class CompletionContext: - """Captures the context within which a completion request has been made.""" - - def __init__( - self, - *, - doc: Document, - location: str, - match: "re.Match", - position: Position, - config: ServerCompletionConfig, - capabilities: ClientCapabilities, - ): - self.doc: Document = doc - """The document within which the completion request was made.""" - - self.location: str = location - """The location type where the request was made. - See :meth:`~esbonio.lsp.rst.RstLanguageServer.get_location_type` for details.""" - - self.match: "re.Match" = match - """The match object describing the site of the completion request.""" - - self.position: Position = position - """The position at which the completion request was made.""" - - self.config: ServerCompletionConfig = config - """User supplied configuration options.""" - - self._client_capabilities: ClientCapabilities = capabilities - - def __repr__(self): - p = f"{self.position.line}:{self.position.character}" - return ( - f"CompletionContext<{self.doc.uri}:{p} ({self.location}) -- {self.match}>" - ) - - @property - def commit_characters_support(self) -> bool: - """Indicates if the client supports commit characters.""" - return get_capability( - self._client_capabilities, - "text_document.completion.completion_item.commit_characters_support", - False, - ) - - @property - def deprecated_support(self) -> bool: - """Indicates if the client supports the deprecated field on a - ``CompletionItem``.""" - return get_capability( - self._client_capabilities, - "text_document.completion.completion_item.deprecated_support", - False, - ) - - @property - def documentation_formats(self) -> List[MarkupKind]: - """The list of documentation formats supported by the client.""" - return get_capability( - self._client_capabilities, - "text_document.completion.completion_item.documentation_format", - [], - ) - - @property - def insert_replace_support(self) -> bool: - """Indicates if the client supports ``InsertReplaceEdit``.""" - return get_capability( - self._client_capabilities, - "text_document.completion.completion_item.insert_replace_support", - False, - ) - - @property - def preselect_support(self) -> bool: - """Indicates if the client supports the preselect field on a - ``CompletionItem``.""" - return get_capability( - self._client_capabilities, - "text_document.completion.completion_item.preselect_support", - False, - ) - - @property - def snippet_support(self) -> bool: - """Indicates if the client supports snippets""" - return get_capability( - self._client_capabilities, - "text_document.completion.completion_item.snippet_support", - False, - ) - - @property - def supported_tags(self) -> List[CompletionItemTag]: - """The list of ``CompletionItemTags`` supported by the client.""" - capabilities = get_capability( - self._client_capabilities, - "text_document.completion.completion_item.tag_support", - None, - ) - - if not capabilities: - return [] - - return capabilities.value_set - - -class DocumentLinkContext: - """Captures the context within which a document link request has been made.""" - - def __init__(self, *, doc: Document, capabilities: ClientCapabilities): - self.doc = doc - """The document within which the document link request was made.""" - - self._client_capabilities = capabilities - - @property - def tooltip_support(self) -> bool: - """Indicates if the client supports tooltips.""" - return get_capability( - self._client_capabilities, - "text_document.document_link.tooltip_support", - False, - ) - - -class DefinitionContext: - """A class that captures the context within which a definition request has been - made.""" - - def __init__( - self, *, doc: Document, location: str, match: "re.Match", position: Position - ): - self.doc = doc - """The document within which the definition request was made.""" - - self.location = location - """The location type where the request was made. - See :meth:`~esbonio.lsp.rst.RstLanguageServer.get_location_type` for details.""" - - self.match = match - """The match object describing the site of the definition request.""" - - self.position = position - """The position at which the definition request was made.""" - - def __repr__(self): - p = f"{self.position.line}:{self.position.character}" - return ( - f"DefinitionContext<{self.doc.uri}:{p} ({self.location}) -- {self.match}>" - ) - - -class ImplementationContext: - """A class that captures the context within which an implementation request has been - made.""" - - def __init__( - self, *, doc: Document, location: str, match: "re.Match", position: Position - ): - self.doc = doc - """The document within which the implementation request was made.""" - - self.location = location - """The location type where the request was made. - See :meth:`~esbonio.lsp.rst.RstLanguageServer.get_location_type` for details.""" - - self.match = match - """The match object describing the site of the implementation request.""" - - self.position = position - """The position at which the implementation request was made.""" - - def __repr__(self): - p = f"{self.position.line}:{self.position.character}" - return f"ImplementationContext<{self.doc.uri}:{p} ({self.location}) -- {self.match}>" - - -class HoverContext: - """A class that captures the context within a hover request has been made.""" - - def __init__( - self, - *, - doc: Document, - location: str, - match: "re.Match", - position: Position, - capabilities: ClientCapabilities, - ): - self.doc = doc - self.location = location - self.match = match - self.position = position - self._client_capabilities = capabilities - - def __repr__(self): - p = f"{self.position.line}:{self.position.character}" - return f"HoverContext<{self.doc.uri}:{p} ({self.location}) -- {self.match}>" - - @property - def content_formats(self) -> List[MarkupKind]: - """The list of content formats supported by the client.""" - return get_capability( - self._client_capabilities, "text_document.hover.content_format", [] - ) - - -class LanguageFeature: - """Base class for language features.""" - - def __init__(self, rst: "RstLanguageServer"): - self.rst = rst - self.logger = rst.logger.getChild(self.__class__.__name__) - - def initialize(self, options: InitializeParams) -> None: - """Called once when the server is first initialized.""" - - def initialized(self, params: InitializedParams) -> None: - """Called once upon receipt of the `initialized` notification from the client.""" - - def on_shutdown(self, *args): - """Called as the server is shutting down.""" - - def save(self, params: DidSaveTextDocumentParams) -> None: - """Called each time a document is saved.""" - - def delete_files(self, params: DeleteFilesParams) -> None: - """Called each time files are deleted.""" - - def code_action(self, params: CodeActionParams) -> List[CodeAction]: - """Called when code actions should be computed.""" - return [] - - hover_triggers: List["re.Pattern"] = [] - - def hover(self, context: HoverContext) -> str: - """Called when request textDocument/hover is sent. - - This method shall return the contents value of textDocument/hover response. - - """ - return "" - - completion_triggers: List["re.Pattern"] = [] - """A list of regular expressions used to determine if the - :meth`~esbonio.lsp.rst.LanguageFeature.complete` method should be called on the - current line.""" - - def complete(self, context: CompletionContext) -> List[CompletionItem]: - """Called if any of the given ``completion_triggers`` match the current line. - - This method should return a list of ``CompletionItem`` objects. - - Parameters - ---------- - context: - The context of the completion request - """ - return [] - - def completion_resolve(self, item: CompletionItem) -> CompletionItem: - """Called with a completion item the user has selected in the UI, - allowing for additional details to be provided e.g. documentation. - - Parameters - ---------- - item: - The completion item to provide additional details for. - """ - return item - - definition_triggers: List["re.Pattern"] = [] - """A list of regular expressions used to determine if the - :meth:`~esbonio.lsp.rst.LanguageFeature.definition` method should be called.""" - - def definition(self, context: DefinitionContext) -> List[Location]: - """Called if any of the given ``definition_triggers`` match the current line. - - This method should return a list of ``Location`` objects. - - Parameters - ---------- - context: - The context of the definition request. - """ - return [] - - implementation_triggers: List["re.Pattern"] = [] - """A list of regular expressions used to determine if the - :meth:`~esbonio.lsp.rst.LanguageFeature.implementation` method should be called.""" - - def implementation(self, context: ImplementationContext) -> List[Location]: - """Called if any of the given ``implementation_triggers`` match the current line. - - This method should return a list of ``Location`` objects. - - Parameters - ---------- - context: - The context of the implementation request. - """ - return [] - - def document_link(self, context: DocumentLinkContext) -> List[DocumentLink]: - """Called whenever a ``textDocument/documentLink`` request is received.""" - return [] - - -class DiagnosticList(collections.UserList): - """A list type dedicated to holding diagnostics. - - This is mainly to ensure that only one instance of a diagnostic ever gets - reported. - """ - - def append(self, item: Diagnostic): - if not isinstance(item, Diagnostic): - raise TypeError("Expected Diagnostic") - - for existing in self.data: - fields = [ - existing.range == item.range, - existing.message == item.message, - existing.severity == item.severity, - existing.code == item.code, - existing.source == item.source, - ] - - if all(fields): - # Item already added, nothing to do. - return - - self.data.append(item) - - -class RstLanguageServer(LanguageServer): - """A generic reStructuredText language server.""" - - def __init__(self, logger: Optional[logging.Logger] = None, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.logger = logger or logging.getLogger(__name__) - """The base logger that should be used for all language sever log entries.""" - - self.converter = self.lsp._converter - """The cattrs converter instance we should use.""" - - self.user_config: InitializationOptions = InitializationOptions() - """The user's configuration.""" - - self._diagnostics: Dict[Tuple[str, str], List[Diagnostic]] = {} - """Where we store and manage diagnostics.""" - - self._loaded_extensions: Dict[str, Any] = {} - """Record of modules that have been loaded.""" - - self._features: Dict[str, LanguageFeature] = {} - """The collection of language features registered with the server.""" - - @property - def configuration(self) -> Dict[str, Any]: - """Return the server's actual configuration.""" - return converter.unstructure(self.user_config) - - def initialize(self, params: InitializeParams): - self.user_config = converter.structure( - params.initialization_options or {}, InitializationOptions - ) - setup_logging(self, self.user_config.server) - - def initialized(self, params: InitializedParams): - pass - - def on_shutdown(self, *args): - pass - - def save(self, params: DidSaveTextDocumentParams): - pass - - def delete_files(self, params: DeleteFilesParams): - pass - - def build(self, force_all: bool = False, filenames: Optional[List[str]] = None): - pass - - def load_extension(self, name: str, setup: Callable): - """Load the given setup function as an extension. - - If an extension with the given ``name`` already exists, the given setup function - will be ignored. - - The ``setup`` function can declare dependencies in the form of type - annotations. - - .. code-block:: python - - from esbonio.lsp.roles import Roles - from esbonio.lsp.sphinx import SphinxLanguageServer - - def esbonio_setup(rst: SphinxLanguageServer, roles: Roles): - ... - - In this example the setup function is requesting instances of the - :class:`~esbonio.lsp.sphinx.SphinxLanguageServer` and the - :class:`~esbonio.lsp.roles.Roles` language feature. - - Parameters - ---------- - name - The name to give the extension - - setup - The setup function to call - """ - - if name in self._loaded_extensions: - self.logger.debug("Skipping extension '%s', already loaded", name) - return - - arguments = _get_setup_arguments(self, setup, name) - if not arguments: - return - - try: - setup(**arguments) - - self.logger.debug("Loaded extension '%s'", name) - self._loaded_extensions[name] = setup - except Exception: - self.logger.error( - "Unable to load extension '%s'\n%s", name, traceback.format_exc() - ) - - def add_feature(self, feature: "LanguageFeature"): - """Register a language feature with the server. - - Parameters - ---------- - feature - The language feature - """ - - key = f"{feature.__module__}.{feature.__class__.__name__}" - self._features[key] = feature - - @typing.overload - def get_feature(self, key: str) -> "Optional[LanguageFeature]": - ... - - @typing.overload - def get_feature(self, key: Type[LF]) -> Optional[LF]: - ... - - def get_feature(self, key): - """Returns the requested language feature if it exists, otherwise it returns - ``None``. - - Parameters - ---------- - key: str | Type[LanguageFeature] - A feature can be referenced either by its class definition (preferred) or by - a string representing the language feature's dotted name e.g. - ``a.b.c.ClassName``. - - .. deprecated:: 0.14.0 - - Passing a string ``key`` to this method is deprecated and will become an - error in ``v1.0``. - """ - - if isinstance(key, str): - warnings.warn( - "Language features should be referenced by their class definition, " - "this will become an error in v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - elif issubclass(key, LanguageFeature): - key = f"{key.__module__}.{key.__name__}" - - else: - raise TypeError("Expected language feature definition") - - return self._features.get(key, None) - - def get_doctree( - self, *, docname: Optional[str] = None, uri: Optional[str] = None - ) -> Optional[Any]: - # Not currently implemented for vanilla docutils projects. - return None - - def get_initial_doctree(self, uri: str) -> Optional[Any]: - """Return the initial doctree corresponding to the specified document. - - An "initial" doctree can be thought of as the abstract syntax tree of a - reStructuredText document. This method disables all role and directives - from being executed, instead they are replaced with nodes that simply - represent that they exist. - - Parameters - ---------- - uri - Returns the doctree that corresponds with the given uri. - """ - - doc = self.workspace.get_document(uri) - loctype = self.get_location_type(doc, Position(line=1, character=0)) - - if loctype != "rst": - return None - - self.logger.debug("Getting initial doctree for: '%s'", Uri.to_fs_path(uri)) - - try: - return read_initial_doctree(doc, self.logger) - except Exception: - self.logger.error(traceback.format_exc()) - return None - - def get_directives(self) -> Dict[str, Directive]: - """Return a dictionary of the known directives - - .. deprecated:: 0.14.2 - - This will be removed in ``v1.0``. - Use the :meth:`~esbonio.lsp.directives.Directives.get_directives` method - instead. - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_directives() is deprecated and will be removed in v1.0." - " Instead call the get_directives() method on the Directives language " - "feature.", - DeprecationWarning, - stacklevel=2, - ) - - feature = self.get_feature("esbonio.lsp.directives.Directives") - if feature is None: - return {} - - return feature.get_directives() # type: ignore - - def get_directive_options(self, name: str) -> Dict[str, Any]: - """Return the options specification for the given directive. - - .. deprecated:: 0.14.2 - - This will be removed in ``v1.0`` - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_directive_options() is deprecated and will be removed in " - "v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - directive = self.get_directives().get(name, None) - if directive is None: - return {} - - return directive.option_spec or {} - - def get_roles(self) -> Dict[str, Any]: - """Return a dictionary of known roles. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0``. - Use the :meth:`~esbonio.lsp.roles.Roles.get_roles` method instead. - """ - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_roles() is deprecated and will be removed in v1.0. " - "Instead call the get_roles() method on the Roles language " - "feature.", - DeprecationWarning, - stacklevel=2, - ) - - feature = self.get_feature("esbonio.lsp.roles.Roles") - if feature is None: - return {} - - return feature.get_roles() # type: ignore - - def get_default_role(self) -> Tuple[Optional[str], Optional[str]]: - """Return the default role for the project.""" - return None, None - - def clear_diagnostics(self, source: str, uri: Optional[str] = None) -> None: - """Clear diagnostics from the given source. - - Parameters - ---------- - source: - The source from which to clear diagnostics. - uri: - If given, clear diagnostics from within just this uri. Otherwise, all - diagnostics from the given source are cleared. - """ - - if uri: - uri = normalise_uri(uri) - - for key in self._diagnostics.keys(): - clear_source = source == key[0] - clear_uri = uri == key[1] or uri is None - - if clear_source and clear_uri: - self._diagnostics[key] = [] - - def add_diagnostics(self, source: str, uri, diagnostic: Diagnostic): - """Add a diagnostic to the given source and uri. - - Parameters - ---------- - source - The source the diagnostics are from - uri - The uri the diagnostics are associated with - diagnostic - The diagnostic to add - """ - key = (source, normalise_uri(uri)) - self._diagnostics.setdefault(key, []).append(diagnostic) - - def set_diagnostics( - self, source: str, uri: str, diagnostics: List[Diagnostic] - ) -> None: - """Set the diagnostics for the given source and uri. - - Parameters - ---------- - source: - The source the diagnostics are from - uri: - The uri the diagnostics are associated with - diagnostics: - The diagnostics themselves - """ - uri = normalise_uri(uri) - self._diagnostics[(source, uri)] = diagnostics - - def sync_diagnostics(self) -> None: - """Update the client with the currently stored diagnostics.""" - - uris = {uri for _, uri in self._diagnostics.keys()} - diagnostics = {uri: DiagnosticList() for uri in uris} - - for (source, uri), diags in self._diagnostics.items(): - for diag in diags: - diag.source = source - diagnostics[uri].append(diag) - - for uri, diag_list in diagnostics.items(): - self.logger.debug("Publishing %d diagnostics for: %s", len(diag_list), uri) - self.publish_diagnostics(uri, diag_list.data) - - def get_location_type(self, document: Document, position: Position) -> str: - """Given a document and a position, return the type of location. - - Returns one of the following values: - - - ``rst``: Indicates that the position is within an reStructuredText document - - ``py``: Indicates that the position is within code in a Python file - - ``docstring``: Indicates that the position is within a docstring in a - Python file. - - If the location type cannot be determined, this function will fall back to - ``rst``. - - Parameters - ---------- - doc - The document associated with the given position - - position - The position to determine the type of - """ - doc = self.workspace.get_document(document.uri) - fpath = pathlib.Path(Uri.to_fs_path(doc.uri)) - - # Prefer the document's language_id, but fallback to file extensions - loctype = getattr(doc, "language_id", None) or fpath.suffix - - if loctype in {".rst", "rst", "restructuredtext"}: - return "rst" - - if loctype in {".py", "py", "python"}: - # Let's count how many pairs of triple quotes are above us in the file - # even => we're outside a docstring - # odd => we're within a docstring - source = self.text_to_position(doc, position) - count = len(TRIPLE_QUOTE.findall(source)) - return "py" if count % 2 == 0 else "docstring" - - # Fallback to rst - self.logger.debug("Unable to determine location type for uri: %s", doc.uri) - return "rst" - - def line_at_position(self, doc: Document, position: Position) -> str: - """Return the contents of the line corresponding to the given position. - - Parameters - ---------- - doc: - The document associated with the given position - position: - The position representing the line to retrieve - """ - - try: - return doc.lines[position.line] - except IndexError: - return "" - - def line_to_position(self, doc: Document, position: Position) -> str: - """Return the contents of the line up until the given position. - - Parameters - ---------- - doc: - The document associated with the given position. - position: - The position representing the line to retrieve. - """ - - line = self.line_at_position(doc, position) - return line[: position.character] - - def preview(self, options: Dict[str, Any]) -> Dict[str, Any]: - """Generate a preview of the documentation.""" - name = self.__class__.__name__ - self.show_message( - f"Previews are not currently supported by {name} based servers" - ) - - return {} - - def text_to_position(self, doc: Document, position: Position) -> str: - """Return the contents of the document up until the given position. - - Parameters - ---------- - doc: - The document associated with the given position - position: - The position representing the point at which to stop gathering text. - """ - idx = doc.offset_at_position(position) - return doc.source[:idx] - - -def normalise_uri(uri: str) -> str: - uri = Uri.from_fs_path(Uri.to_fs_path(uri)) - - # Paths on windows are case insensitive. - if IS_WIN: - uri = uri.lower() - - return uri - - -def _get_setup_arguments( - server: RstLanguageServer, setup: Callable, modname: str -) -> Optional[Dict[str, Any]]: - """Given a setup function, try to construct the collection of arguments to pass to - it. - """ - annotations = typing.get_type_hints(setup) - parameters = { - p.name: annotations[p.name] - for p in inspect.signature(setup).parameters.values() - } - - args = {} - for name, type_ in parameters.items(): - if issubclass(server.__class__, type_): - args[name] = server - continue - - if issubclass(type_, LanguageFeature): - # Try and obtain an instance of the requested language feature. - feature = server.get_feature(type_) - if feature is not None: - args[name] = feature - continue - - server.logger.debug( - "Skipping extension '%s', server missing requested feature: '%s'", - modname, - type_, - ) - return None - - server.logger.error( - "Skipping extension '%s', parameter '%s' has unsupported type: '%s'", - modname, - name, - type_, - ) - return None - - return args - - -cli = setup_cli("esbonio.lsp.rst", "Esbonio's reStructuredText language server.") -cli.set_defaults(modules=DEFAULT_MODULES) -cli.set_defaults(server_cls=RstLanguageServer) diff --git a/lib/esbonio/esbonio/lsp/rst/__main__.py b/lib/esbonio/esbonio/lsp/rst/__main__.py deleted file mode 100644 index eb49a0162..000000000 --- a/lib/esbonio/esbonio/lsp/rst/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys - -from esbonio.cli import main -from esbonio.lsp.rst import cli - -sys.exit(main(cli)) diff --git a/lib/esbonio/esbonio/lsp/rst/config.py b/lib/esbonio/esbonio/lsp/rst/config.py deleted file mode 100644 index 054576c79..000000000 --- a/lib/esbonio/esbonio/lsp/rst/config.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import List -from typing import Literal - -import attrs - - -@attrs.define -class ServerCompletionConfig: - """Configuration options for the server that control completion behavior.""" - - preferred_insert_behavior: Literal["insert", "replace"] = attrs.field( - default="replace" - ) - """This option indicates if the user prefers we use ``insertText`` or ``textEdit`` - when rendering ``CompletionItems``.""" - - -@attrs.define -class ServerConfig: - """Configuration options for the server.""" - - completion: ServerCompletionConfig = attrs.field(factory=ServerCompletionConfig) - """Configuration values that affect completion""" - - enable_scroll_sync: bool = attrs.field(default=False) - """Enable custom transformation to add classes with line numbers""" - - enable_live_preview: bool = attrs.field(default=False) - """Set it to True if you want to build Sphinx app on change event""" - - log_filter: List[str] = attrs.field(factory=list) - """A list of logger names to restrict output to.""" - - log_level: str = attrs.field(default="error") - """The logging level of server messages to display.""" - - show_deprecation_warnings: bool = attrs.field(default=False) - """Developer flag to enable deprecation warnings.""" - - -@attrs.define -class InitializationOptions: - """The initialization options we can expect to receive from a client.""" - - server: ServerConfig = attrs.field(factory=ServerConfig) - """The ``esbonio.server.*`` namespace of options.""" diff --git a/lib/esbonio/esbonio/lsp/rst/directives.json b/lib/esbonio/esbonio/lsp/rst/directives.json deleted file mode 100644 index ecafa98e0..000000000 --- a/lib/esbonio/esbonio/lsp/rst/directives.json +++ /dev/null @@ -1,2368 +0,0 @@ -{ - "sectnum(docutils.parsers.rst.directives.parts.Sectnum)": { - "is_markdown": true, - "description": [ - "## Automatic Section Numbering", - "", - "| | |", - "|-|-|", - "| Directive Type | \"sectnum\" or \"section-numbering\" (synonyms) |", - "| Doctree Elements | [pending](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#pending), [generated](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#generated) |", - "| Directive Arguments | None. |", - "| Directive Options | Possible (see below). |", - "| Directive Content | None. |", - "| Configuration Setting | [sectnum_xform](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#sectnum-xform) |", - "", - "The \"sectnum\" (or \"section-numbering\") directive automatically numbers", - "sections and subsections in a document (if not disabled by the", - "`--no-section-numbering` command line option or the [sectnum_xform](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#sectnum-xform)", - "configuration setting).", - "", - "Section numbers are of the \"multiple enumeration\" form, where each", - "level has a number, separated by periods. For example, the title of section", - "1, subsection 2, subsubsection 3 would have \"1.2.3\" prefixed.", - "", - "The \"sectnum\" directive does its work in two passes: the initial parse", - "and a transform. During the initial parse, a \"pending\" element is", - "generated which acts as a placeholder, storing any options internally.", - "At a later stage in the processing, the \"pending\" element triggers a", - "transform, which adds section numbers to titles. Section numbers are", - "enclosed in a \"generated\" element, and titles have their \"auto\"", - "attribute set to \"1\".", - "", - "The following options are recognized:", - "", - "`depth`: integer", - "The number of section levels that are numbered by this directive.", - "The default is unlimited depth.", - "", - "`prefix`: string", - "An arbitrary string that is prefixed to the automatically", - "generated section numbers. It may be something like \"3.2.\", which", - "will produce \"3.2.1\", \"3.2.2\", \"3.2.2.1\", and so on. Note that", - "any separating punctuation (in the example, a period, \".\") must be", - "explicitly provided. The default is no prefix.", - "", - "`suffix`: string", - "An arbitrary string that is appended to the automatically", - "generated section numbers. The default is no suffix.", - "", - "`start`: integer", - "The value that will be used for the first section number.", - "Combined with `prefix`, this may be used to force the right", - "numbering for a document split over several source files. The", - "default is 1." - ], - "options": { - "depth": "integer\nThe number of section levels that are numbered by this directive.\nThe default is unlimited depth.\n", - "prefix": "string\nAn arbitrary string that is prefixed to the automatically\ngenerated section numbers. It may be something like \"3.2.\", which\nwill produce \"3.2.1\", \"3.2.2\", \"3.2.2.1\", and so on. Note that\nany separating punctuation (in the example, a period, \".\") must be\nexplicitly provided. The default is no prefix.\n", - "suffix": "string\nAn arbitrary string that is appended to the automatically\ngenerated section numbers. The default is no suffix.\n", - "start": "integer\nThe value that will be used for the first section number.\nCombined with `prefix`, this may be used to force the right\nnumbering for a document split over several source files. The\ndefault is 1.\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#automatic-section-numbering", - "license": "https://docutils.sourceforge.io/docs/" - }, - "attention(docutils.parsers.rst.directives.admonitions.Attention)": { - "is_markdown": true, - "description": [ - "## Specific Admonitions", - "", - "| | |", - "|-|-|", - "| Directive Types | \"attention\", \"caution\", \"danger\", \"error\", \"hint\",", - "\"important\", \"note\", \"tip\", \"warning\", \"admonition\" |", - "| Doctree Elements | attention, caution, danger, error, hint, important,", - "note, tip, warning, [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "Admonitions are specially marked \"topics\" that can appear anywhere an", - "ordinary body element can. They contain arbitrary body elements.", - "Typically, an admonition is rendered as an offset block in a document,", - "sometimes outlined or shaded, with a title matching the admonition", - "type. For example:", - "", - "```", - ".. DANGER::", - " Beware killer rabbits!", - "```", - "", - "This directive might be rendered something like this:", - "", - "```", - "+------------------------+", - "| !DANGER! |", - "| |", - "| Beware killer rabbits! |", - "+------------------------+", - "```", - "", - "The following admonition directives have been implemented:", - "- attention", - "- caution", - "- danger", - "- error", - "- hint", - "- important", - "- note", - "- tip", - "- warning", - "", - "Any text immediately following the directive indicator (on the same", - "line and/or indented on following lines) is interpreted as a directive", - "block and is parsed for normal body elements. For example, the", - "following \"note\" admonition directive contains one paragraph and a", - "bullet list consisting of two list items:", - "", - "```", - ".. note:: This is a note admonition.", - " This is the second line of the first paragraph.", - "", - " - The note contains all indented body elements", - " following.", - " - It includes this bullet list.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#attention", - "license": "https://docutils.sourceforge.io/docs/" - }, - "caution(docutils.parsers.rst.directives.admonitions.Caution)": { - "is_markdown": true, - "description": [ - "## Specific Admonitions", - "", - "| | |", - "|-|-|", - "| Directive Types | \"attention\", \"caution\", \"danger\", \"error\", \"hint\",", - "\"important\", \"note\", \"tip\", \"warning\", \"admonition\" |", - "| Doctree Elements | attention, caution, danger, error, hint, important,", - "note, tip, warning, [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "Admonitions are specially marked \"topics\" that can appear anywhere an", - "ordinary body element can. They contain arbitrary body elements.", - "Typically, an admonition is rendered as an offset block in a document,", - "sometimes outlined or shaded, with a title matching the admonition", - "type. For example:", - "", - "```", - ".. DANGER::", - " Beware killer rabbits!", - "```", - "", - "This directive might be rendered something like this:", - "", - "```", - "+------------------------+", - "| !DANGER! |", - "| |", - "| Beware killer rabbits! |", - "+------------------------+", - "```", - "", - "The following admonition directives have been implemented:", - "- attention", - "- caution", - "- danger", - "- error", - "- hint", - "- important", - "- note", - "- tip", - "- warning", - "", - "Any text immediately following the directive indicator (on the same", - "line and/or indented on following lines) is interpreted as a directive", - "block and is parsed for normal body elements. For example, the", - "following \"note\" admonition directive contains one paragraph and a", - "bullet list consisting of two list items:", - "", - "```", - ".. note:: This is a note admonition.", - " This is the second line of the first paragraph.", - "", - " - The note contains all indented body elements", - " following.", - " - It includes this bullet list.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#caution", - "license": "https://docutils.sourceforge.io/docs/" - }, - "code(docutils.parsers.rst.directives.body.CodeBlock)": { - "is_markdown": true, - "description": [ - "## Code", - "", - "| | |", - "|-|-|", - "| Directive Type | \"code\" |", - "| Doctree Element | [literal_block](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#literal-block), [inline elements](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#inline-elements) |", - "| Directive Arguments | One, optional (formal language). |", - "| Directive Options | name, class, number-lines. |", - "| Directive Content | Becomes the body of the literal block. |", - "| Configuration Setting | [syntax_highlight](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#syntax-highlight). |", - "", - "The \"code\" directive constructs a literal block. If the code language is", - "specified, the content is parsed by the [Pygments](https://pygments.org/) syntax highlighter and", - "tokens are stored in nested [inline elements](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#inline-elements) with class arguments", - "according to their syntactic category. The actual highlighting requires", - "a style-sheet (e.g. one [generated by Pygments](https://pygments.org/docs/cmdline/#generating-styles), see the", - "[sandbox/stylesheets](https://docutils.sourceforge.io/sandbox/stylesheets/) for examples).", - "", - "The parsing can be turned off with the [syntax_highlight](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#syntax-highlight) configuration", - "setting and command line option or by specifying the language as [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33)", - "option instead of directive argument. This also avoids warnings", - "when [Pygments](https://pygments.org/) is not installed or the language is not in the", - "[supported languages and markup formats](https://pygments.org/languages/).", - "", - "For inline code, use the [\"code\" role](https:/docutils.sourceforge.io/docs/ref/rst/roles.html#code).", - "", - "The following options are recognized:", - "", - "`number-lines`: [integer] (start line number)", - "Precede every line with a line number.", - "The optional argument is the number of the first line (default 1).", - "", - "and the common options [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) and [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name).", - "", - "`Example::`: ", - "The content of the following directive", - "", - "```", - ".. code:: python", - "", - " def my_function():", - " \"just a test\"", - " print 8/2", - "```", - "", - "is parsed and marked up as Python source code." - ], - "options": { - "number-lines": "[integer] (start line number)\nPrecede every line with a line number.\nThe optional argument is the number of the first line (default 1).\n", - "Example::": "\nThe content of the following directive\n\n```\n.. code:: python\n\n def my_function():\n \"just a test\"\n print 8/2\n```\n\nis parsed and marked up as Python source code.\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#code", - "license": "https://docutils.sourceforge.io/docs/" - }, - "compound(docutils.parsers.rst.directives.body.Compound)": { - "is_markdown": true, - "description": [ - "## Compound Paragraph", - "", - "| | |", - "|-|-|", - "| Directive Type | \"compound\" |", - "| Doctree Element | [compound](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#compound) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "The \"compound\" directive is used to create a compound paragraph, which", - "is a single logical paragraph containing multiple physical body", - "elements such as simple paragraphs, literal blocks, tables, lists,", - "etc., instead of directly containing text and inline elements. For", - "example:", - "", - "```", - ".. compound::", - "", - " The 'rm' command is very dangerous. If you are logged", - " in as root and enter ::", - "", - " cd /", - " rm -rf *", - "", - " you will erase the entire contents of your file system.", - "```", - "", - "In the example above, a literal block is \"embedded\" within a sentence", - "that begins in one physical paragraph and ends in another.", - "", - "The \"compound\" directive is not a generic block-level container", - "like HTML's `
` element. Do not use it only to group a", - "sequence of elements, or you may get unexpected results.", - "", - "If you need a generic block-level container, please use the", - "[container](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#container) directive, described below.", - "", - "Compound paragraphs are typically rendered as multiple distinct text", - "blocks, with the possibility of variations to emphasize their logical", - "unity:", - "- If paragraphs are rendered with a first-line indent, only the first", - "physical paragraph of a compound paragraph should have that indent", - "-- second and further physical paragraphs should omit the indents;", - "- vertical spacing between physical elements may be reduced;", - "- and so on." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#compound-paragraph", - "license": "https://docutils.sourceforge.io/docs/" - }, - "container(docutils.parsers.rst.directives.body.Container)": { - "is_markdown": true, - "description": [ - "## Container", - "", - "| | |", - "|-|-|", - "| Directive Type | \"container\" |", - "| Doctree Element | [container](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#container) |", - "| Directive Arguments | One or more, optional (class names). |", - "| Directive Options | [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "The \"container\" directive surrounds its contents (arbitrary body", - "elements) with a generic block-level \"container\" element. Combined", - "with the optional \"[classes](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#classes)\" attribute argument(s), this is an", - "extension mechanism for users & applications. For example:", - "", - "```", - ".. container:: custom", - "", - " This paragraph might be rendered in a custom way.", - "```", - "", - "Parsing the above results in the following pseudo-XML:", - "", - "```", - "", - " ", - " This paragraph might be rendered in a custom way.", - "```", - "", - "The \"container\" directive is the equivalent of HTML's `
`", - "element. It may be used to group a sequence of elements for user- or", - "application-specific purposes." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#container", - "license": "https://docutils.sourceforge.io/docs/" - }, - "csv-table(docutils.parsers.rst.directives.tables.CSVTable)": { - "is_markdown": true, - "description": [ - "## CSV Table", - "", - "| | |", - "|-|-|", - "| Directive Type | \"csv-table\" |", - "| Doctree Element | [table](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#table) |", - "| Directive Arguments | One, optional (table title). |", - "| Directive Options | Possible (see below). |", - "| Directive Content | A CSV (comma-separated values) table. |", - "", - "The \"csv-table\" directive's \":file:\" and \":url:\" options represent", - "a potential security holes. They can be disabled with the", - "\"[file_insertion_enabled](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#file-insertion-enabled)\" runtime setting.", - "", - "The \"csv-table\" directive is used to create a table from CSV", - "(comma-separated values) data. CSV is a common data format generated", - "by spreadsheet applications and commercial databases. The data may be", - "internal (an integral part of the document) or external (a separate", - "file).", - "- Block markup and inline markup within cells is supported. Line ends", - "are recognized within cells.", - "- There is no support for checking that the number of columns in each", - "row is the same. The directive automatically adds empty entries at", - "the end of short rows.", - "Add \"strict\" option to verify input?", - "Example:", - "", - "```", - ".. csv-table:: Frozen Delights!", - " :header: \"Treat\", \"Quantity\", \"Description\"", - " :widths: 15, 10, 30", - "", - " \"Albatross\", 2.99, \"On a stick!\"", - " \"Crunchy Frog\", 1.49, \"If we took the bones out, it wouldn't be", - " crunchy, now would it?\"", - " \"Gannet Ripple\", 1.99, \"On a stick!\"", - "```", - "", - "The following options are recognized:", - "", - "`align`: \"left\", \"center\", or \"right\"", - "The horizontal alignment of the table. (New in Docutils 0.13)", - "", - "`delim`: char | \"tab\" | \"space\" 4", - "A one-character string5 used to separate fields.", - "Defaults to `,` (comma). May be specified as a Unicode code", - "point; see the [unicode](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#unicode) directive for syntax details.", - "", - "`encoding`: string", - "The text encoding of the external CSV data (file or URL).", - "Defaults to the document's [input_encoding](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#input-encoding).", - "", - "`escape`: char", - "A one-character5 string used to escape the", - "delimiter or quote characters. May be specified as a Unicode", - "code point; see the [unicode](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#unicode) directive for syntax details. Used", - "when the delimiter is used in an unquoted field, or when quote", - "characters are used within a field. The default is to double-up", - "the character, e.g. \"He said, \"\"Hi!\"\"\"", - "Add another possible value, \"double\", to explicitly indicate", - "the default case?", - "`file`: string (newlines removed)", - "The local filesystem path to a CSV data file.", - "", - "`header`: CSV data", - "Supplemental data for the table header, added independently of and", - "before any `header-rows` from the main CSV data. Must use the", - "same CSV format as the main CSV data.", - "", - "`header-rows`: integer", - "The number of rows of CSV data to use in the table header.", - "Defaults to 0.", - "", - "`keepspace`: flag (empty)", - "Treat whitespace immediately following the delimiter as", - "significant. The default is to ignore such whitespace.", - "", - "`quote`: char", - "A one-character string5 used to quote elements", - "containing the delimiter or which start with the quote", - "character. Defaults to `\"` (quote). May be specified as a", - "Unicode code point; see the [unicode](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#unicode) directive for syntax", - "details.", - "", - "`stub-columns`: integer", - "The number of table columns to use as stubs (row titles, on the", - "left). Defaults to 0.", - "", - "`url`: string (whitespace removed)", - "An Internet URL reference to a CSV data file.", - "", - "`widths`: integer [integer...] or \"auto\"", - "A list of relative column widths.", - "The default is equal-width columns (100%/#columns).", - "", - "\"auto\" delegates the determination of column widths to the backend", - "(LaTeX, the HTML browser, ...).", - "", - "`width`: [length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units) or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units)", - "Sets the width of the table to the specified length or percentage", - "of the line width. If omitted, the renderer determines the width", - "of the table based on its contents or the column `widths`.", - "", - "and the common options [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) and [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name).", - "4", - "Whitespace delimiters are supported only for external", - "CSV files.", - "5", - "With Python\u00a02, the values for the `delimiter`,", - "`quote`, and `escape` options must be ASCII characters. (The csv", - "module does not support Unicode and all non-ASCII characters are", - "encoded as multi-byte utf-8 string). This limitation does not exist", - "under Python\u00a03." - ], - "options": { - "align": "\"left\", \"center\", or \"right\"\nThe horizontal alignment of the table. (New in Docutils 0.13)\n", - "delim": "char | \"tab\" | \"space\" 4\nA one-character string5 used to separate fields.\nDefaults to `,` (comma). May be specified as a Unicode code\npoint; see the [unicode](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#unicode) directive for syntax details.\n", - "encoding": "string\nThe text encoding of the external CSV data (file or URL).\nDefaults to the document's [input_encoding](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#input-encoding).\n", - "escape": "char\nA one-character5 string used to escape the\ndelimiter or quote characters. May be specified as a Unicode\ncode point; see the [unicode](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#unicode) directive for syntax details. Used\nwhen the delimiter is used in an unquoted field, or when quote\ncharacters are used within a field. The default is to double-up\nthe character, e.g. \"He said, \"\"Hi!\"\"\"\nAdd another possible value, \"double\", to explicitly indicate\nthe default case?", - "file": "string (newlines removed)\nThe local filesystem path to a CSV data file.\n", - "header": "CSV data\nSupplemental data for the table header, added independently of and\nbefore any `header-rows` from the main CSV data. Must use the\nsame CSV format as the main CSV data.\n", - "header-rows": "integer\nThe number of rows of CSV data to use in the table header.\nDefaults to 0.\n", - "keepspace": "flag (empty)\nTreat whitespace immediately following the delimiter as\nsignificant. The default is to ignore such whitespace.\n", - "quote": "char\nA one-character string5 used to quote elements\ncontaining the delimiter or which start with the quote\ncharacter. Defaults to `\"` (quote). May be specified as a\nUnicode code point; see the [unicode](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#unicode) directive for syntax\ndetails.\n", - "stub-columns": "integer\nThe number of table columns to use as stubs (row titles, on the\nleft). Defaults to 0.\n", - "url": "string (whitespace removed)\nAn Internet URL reference to a CSV data file.\n", - "widths": "integer [integer...] or \"auto\"\nA list of relative column widths.\nThe default is equal-width columns (100%/#columns).\n\n\"auto\" delegates the determination of column widths to the backend\n(LaTeX, the HTML browser, ...).\n", - "width": "[length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units) or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units)\nSets the width of the table to the specified length or percentage\nof the line width. If omitted, the renderer determines the width\nof the table based on its contents or the column `widths`.\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#csv-table", - "license": "https://docutils.sourceforge.io/docs/" - }, - "danger(docutils.parsers.rst.directives.admonitions.Danger)": { - "is_markdown": true, - "description": [ - "## Specific Admonitions", - "", - "| | |", - "|-|-|", - "| Directive Types | \"attention\", \"caution\", \"danger\", \"error\", \"hint\",", - "\"important\", \"note\", \"tip\", \"warning\", \"admonition\" |", - "| Doctree Elements | attention, caution, danger, error, hint, important,", - "note, tip, warning, [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "Admonitions are specially marked \"topics\" that can appear anywhere an", - "ordinary body element can. They contain arbitrary body elements.", - "Typically, an admonition is rendered as an offset block in a document,", - "sometimes outlined or shaded, with a title matching the admonition", - "type. For example:", - "", - "```", - ".. DANGER::", - " Beware killer rabbits!", - "```", - "", - "This directive might be rendered something like this:", - "", - "```", - "+------------------------+", - "| !DANGER! |", - "| |", - "| Beware killer rabbits! |", - "+------------------------+", - "```", - "", - "The following admonition directives have been implemented:", - "- attention", - "- caution", - "- danger", - "- error", - "- hint", - "- important", - "- note", - "- tip", - "- warning", - "", - "Any text immediately following the directive indicator (on the same", - "line and/or indented on following lines) is interpreted as a directive", - "block and is parsed for normal body elements. For example, the", - "following \"note\" admonition directive contains one paragraph and a", - "bullet list consisting of two list items:", - "", - "```", - ".. note:: This is a note admonition.", - " This is the second line of the first paragraph.", - "", - " - The note contains all indented body elements", - " following.", - " - It includes this bullet list.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#danger", - "license": "https://docutils.sourceforge.io/docs/" - }, - "date(docutils.parsers.rst.directives.misc.Date)": { - "is_markdown": true, - "description": [ - "## Date", - "", - "| | |", - "|-|-|", - "| Directive Type | \"date\" |", - "| Doctree Element | Text |", - "| Directive Arguments | One, optional (date format). |", - "| Directive Options | None. |", - "| Directive Content | None. |", - "", - "The \"date\" directive generates the current local date and inserts it", - "into the document as text. This directive may be used in substitution", - "definitions only.", - "", - "The optional directive content is interpreted as the desired date", - "format, using the same codes as Python's [time.strftime()](https://docs.python.org/3/library/time.html#time.strftime) function. The", - "default format is \"%Y-%m-%d\" (ISO 8601 date), but time fields can also", - "be used. Examples:", - "", - "```", - ".. |date| date::", - ".. |time| date:: %H:%M", - "", - "Today's date is |date|.", - "", - "This document was generated on |date| at |time|.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#date", - "license": "https://docutils.sourceforge.io/docs/" - }, - "default-role(docutils.parsers.rst.directives.misc.DefaultRole)": { - "is_markdown": true, - "description": [ - "## Setting the Default Interpreted Text Role", - "", - "| | |", - "|-|-|", - "| Directive Type | \"default-role\" |", - "| Doctree Element | None; affects subsequent parsing. |", - "| Directive Arguments | One, optional (new default role name). |", - "| Directive Options | None. |", - "| Directive Content | None. |", - "", - "The \"default-role\" directive sets the default interpreted text role,", - "the role that is used for interpreted text without an explicit role.", - "For example, after setting the default role like this:", - "", - "```", - ".. default-role:: subscript", - "```", - "", - "any subsequent use of implicit-role interpreted text in the document", - "will use the \"subscript\" role:", - "", - "```", - "An example of a `default` role.", - "```", - "", - "This will be parsed into the following document tree fragment:", - "", - "```", - "", - " An example of a", - " ", - " default", - " role.", - "```", - "", - "Custom roles may be used (see the \"[role](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#role)\" directive above), but it", - "must have been declared in a document before it can be set as the", - "default role. See the [reStructuredText Interpreted Text Roles](https:/docutils.sourceforge.io/docs/ref/rst/roles.html)", - "document for details of built-in roles.", - "", - "The directive may be used without an argument to restore the initial", - "default interpreted text role, which is application-dependent. The", - "initial default interpreted text role of the standard reStructuredText", - "parser is \"title-reference\"." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#default-role", - "license": "https://docutils.sourceforge.io/docs/" - }, - "epigraph(docutils.parsers.rst.directives.body.Epigraph)": { - "is_markdown": true, - "description": [ - "## Epigraph", - "", - "| | |", - "|-|-|", - "| Directive Type | \"epigraph\" |", - "| Doctree Element | [block_quote](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#block-quote) |", - "| Directive Arguments | None. |", - "| Directive Options | None. |", - "| Directive Content | Interpreted as the body of the block quote. |", - "", - "An epigraph is an apposite (suitable, apt, or pertinent) short", - "inscription, often a quotation or poem, at the beginning of a document", - "or section.", - "", - "The \"epigraph\" directive produces an \"epigraph\"-class block quote.", - "For example, this input:", - "", - "```", - ".. epigraph::", - "", - " No matter where you go, there you are.", - "", - " -- Buckaroo Banzai", - "```", - "", - "becomes this document tree fragment:", - "", - "```", - "", - " ", - " No matter where you go, there you are.", - " ", - " Buckaroo Banzai", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#epigraph", - "license": "https://docutils.sourceforge.io/docs/" - }, - "error(docutils.parsers.rst.directives.admonitions.Error)": { - "is_markdown": true, - "description": [ - "## Specific Admonitions", - "", - "| | |", - "|-|-|", - "| Directive Types | \"attention\", \"caution\", \"danger\", \"error\", \"hint\",", - "\"important\", \"note\", \"tip\", \"warning\", \"admonition\" |", - "| Doctree Elements | attention, caution, danger, error, hint, important,", - "note, tip, warning, [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "Admonitions are specially marked \"topics\" that can appear anywhere an", - "ordinary body element can. They contain arbitrary body elements.", - "Typically, an admonition is rendered as an offset block in a document,", - "sometimes outlined or shaded, with a title matching the admonition", - "type. For example:", - "", - "```", - ".. DANGER::", - " Beware killer rabbits!", - "```", - "", - "This directive might be rendered something like this:", - "", - "```", - "+------------------------+", - "| !DANGER! |", - "| |", - "| Beware killer rabbits! |", - "+------------------------+", - "```", - "", - "The following admonition directives have been implemented:", - "- attention", - "- caution", - "- danger", - "- error", - "- hint", - "- important", - "- note", - "- tip", - "- warning", - "", - "Any text immediately following the directive indicator (on the same", - "line and/or indented on following lines) is interpreted as a directive", - "block and is parsed for normal body elements. For example, the", - "following \"note\" admonition directive contains one paragraph and a", - "bullet list consisting of two list items:", - "", - "```", - ".. note:: This is a note admonition.", - " This is the second line of the first paragraph.", - "", - " - The note contains all indented body elements", - " following.", - " - It includes this bullet list.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#error", - "license": "https://docutils.sourceforge.io/docs/" - }, - "figure(docutils.parsers.rst.directives.images.Figure)": { - "is_markdown": true, - "description": [ - "## Figure", - "", - "| | |", - "|-|-|", - "| Directive Type | \"figure\" |", - "| Doctree Elements | [figure](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#figure), [image](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#image), [caption](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#caption), [legend](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#legend) |", - "| Directive Arguments | One, required (image URI). |", - "| Directive Options | Possible (see below). |", - "| Directive Content | Interpreted as the figure caption and an optional", - "legend. |", - "", - "A \"figure\" consists of [image](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#image) data (including [image options](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#image-options)), an optional", - "caption (a single paragraph), and an optional legend (arbitrary body", - "elements). For page-based output media, figures might float to a different", - "position if this helps the page layout.", - "", - "```", - ".. figure:: picture.png", - " :scale: 50 %", - " :alt: map to buried treasure", - "", - " This is the caption of the figure (a simple paragraph).", - "", - " The legend consists of all elements after the caption. In this", - " case, the legend consists of this paragraph and the following", - " table:", - "", - " +-----------------------+-----------------------+", - " | Symbol | Meaning |", - " +=======================+=======================+", - " | .. image:: tent.png | Campground |", - " +-----------------------+-----------------------+", - " | .. image:: waves.png | Lake |", - " +-----------------------+-----------------------+", - " | .. image:: peak.png | Mountain |", - " +-----------------------+-----------------------+", - "```", - "", - "There must be blank lines before the caption paragraph and before the", - "legend. To specify a legend without a caption, use an empty comment", - "(\"..\") in place of the caption.", - "", - "The \"figure\" directive supports all of the options of the \"image\"", - "directive (see [image options](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#image-options) above). These options (except", - "\"align\") are passed on to the contained image.", - "", - "`align`: \"left\", \"center\", or \"right\"", - "The horizontal alignment of the figure, allowing the image to", - "float and have the text flow around it. The specific behavior", - "depends upon the browser or rendering software used.", - "", - "In addition, the following options are recognized:", - "", - "`figwidth`: \"image\", [length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units), or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units) of current line width", - "The width of the figure.", - "Limits the horizontal space used by the figure.", - "A special value of \"image\" is allowed, in which case the", - "included image's actual width is used (requires the [Python Imaging", - "Library](https://pypi.org/project/Pillow/)). If the image file is not found or the required software is", - "unavailable, this option is ignored.", - "", - "Sets the \"width\" attribute of the \"figure\" doctree element.", - "", - "This option does not scale the included image; use the \"width\"", - "[image](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#image) option for that.", - "", - "```", - "+---------------------------+", - "| figure |", - "| |", - "|<------ figwidth --------->|", - "| |", - "| +---------------------+ |", - "| | image | |", - "| | | |", - "| |<--- width --------->| |", - "| +---------------------+ |", - "| |", - "|The figure's caption should|", - "|wrap at this width. |", - "+---------------------------+", - "```", - "", - "`figclass`: text", - "Set a [\"classes\"](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#classes) attribute value on the figure element. See the", - "[class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) directive below." - ], - "options": { - "align": "\"left\", \"center\", or \"right\"\nThe horizontal alignment of the figure, allowing the image to\nfloat and have the text flow around it. The specific behavior\ndepends upon the browser or rendering software used.\n", - "figwidth": "\"image\", [length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units), or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units) of current line width\nThe width of the figure.\nLimits the horizontal space used by the figure.\nA special value of \"image\" is allowed, in which case the\nincluded image's actual width is used (requires the [Python Imaging\nLibrary](https://pypi.org/project/Pillow/)). If the image file is not found or the required software is\nunavailable, this option is ignored.\n\nSets the \"width\" attribute of the \"figure\" doctree element.\n\nThis option does not scale the included image; use the \"width\"\n[image](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#image) option for that.\n\n```\n+---------------------------+\n| figure |\n| |\n|<------ figwidth --------->|\n| |\n| +---------------------+ |\n| | image | |\n| | | |\n| |<--- width --------->| |\n| +---------------------+ |\n| |\n|The figure's caption should|\n|wrap at this width. |\n+---------------------------+\n```\n", - "figclass": "text\nSet a [\"classes\"](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#classes) attribute value on the figure element. See the\n[class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) directive below.\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#figure", - "license": "https://docutils.sourceforge.io/docs/" - }, - "footer(docutils.parsers.rst.directives.parts.Footer)": { - "is_markdown": true, - "description": [ - "## Document Header & Footer", - "", - "| | |", - "|-|-|", - "| Directive Types | \"header\" and \"footer\" |", - "| Doctree Elements | [decoration](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#decoration), header, footer |", - "| Directive Arguments | None. |", - "| Directive Options | None. |", - "| Directive Content | Interpreted as body elements. |", - "", - "The \"header\" and \"footer\" directives create document decorations,", - "useful for page navigation, notes, time/datestamp, etc. For example:", - "", - "```", - ".. header:: This space for rent.", - "```", - "", - "This will add a paragraph to the document header, which will appear at", - "the top of the generated web page or at the top of every printed page.", - "", - "These directives may be used multiple times, cumulatively. There is", - "currently support for only one header and footer.", - "", - "While it is possible to use the \"header\" and \"footer\" directives to", - "create navigational elements for web pages, you should be aware", - "that Docutils is meant to be used for document processing, and", - "that a navigation bar is not typically part of a document.", - "", - "Thus, you may soon find Docutils' abilities to be insufficient for", - "these purposes. At that time, you should consider using a", - "documentation generator like [Sphinx](http://sphinx-doc.org/) rather than the \"header\" and", - "\"footer\" directives.", - "", - "In addition to the use of these directives to populate header and", - "footer content, content may also be added automatically by the", - "processing system. For example, if certain runtime settings are", - "enabled, the document footer is populated with processing information", - "such as a datestamp, a link to [the Docutils website](https://docutils.sourceforge.io), etc." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#footer", - "license": "https://docutils.sourceforge.io/docs/" - }, - "header(docutils.parsers.rst.directives.parts.Header)": { - "is_markdown": true, - "description": [ - "## Document Header & Footer", - "", - "| | |", - "|-|-|", - "| Directive Types | \"header\" and \"footer\" |", - "| Doctree Elements | [decoration](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#decoration), header, footer |", - "| Directive Arguments | None. |", - "| Directive Options | None. |", - "| Directive Content | Interpreted as body elements. |", - "", - "The \"header\" and \"footer\" directives create document decorations,", - "useful for page navigation, notes, time/datestamp, etc. For example:", - "", - "```", - ".. header:: This space for rent.", - "```", - "", - "This will add a paragraph to the document header, which will appear at", - "the top of the generated web page or at the top of every printed page.", - "", - "These directives may be used multiple times, cumulatively. There is", - "currently support for only one header and footer.", - "", - "While it is possible to use the \"header\" and \"footer\" directives to", - "create navigational elements for web pages, you should be aware", - "that Docutils is meant to be used for document processing, and", - "that a navigation bar is not typically part of a document.", - "", - "Thus, you may soon find Docutils' abilities to be insufficient for", - "these purposes. At that time, you should consider using a", - "documentation generator like [Sphinx](http://sphinx-doc.org/) rather than the \"header\" and", - "\"footer\" directives.", - "", - "In addition to the use of these directives to populate header and", - "footer content, content may also be added automatically by the", - "processing system. For example, if certain runtime settings are", - "enabled, the document footer is populated with processing information", - "such as a datestamp, a link to [the Docutils website](https://docutils.sourceforge.io), etc." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#header", - "license": "https://docutils.sourceforge.io/docs/" - }, - "highlights(docutils.parsers.rst.directives.body.Highlights)": { - "is_markdown": true, - "description": [ - "## Highlights", - "", - "| | |", - "|-|-|", - "| Directive Type | \"highlights\" |", - "| Doctree Element | [block_quote](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#block-quote) |", - "| Directive Arguments | None. |", - "| Directive Options | None. |", - "| Directive Content | Interpreted as the body of the block quote. |", - "", - "Highlights summarize the main points of a document or section, often", - "consisting of a list.", - "", - "The \"highlights\" directive produces a \"highlights\"-class block quote.", - "See [Epigraph](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#epigraph) above for an analogous example." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#highlights", - "license": "https://docutils.sourceforge.io/docs/" - }, - "admonition(docutils.parsers.rst.directives.admonitions.Admonition)": { - "is_markdown": true, - "description": [ - "## Generic Admonition", - "", - "| | |", - "|-|-|", - "| Directive Type | \"admonition\" |", - "| Doctree Elements | [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | One, required (admonition title) |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "This is a generic, titled admonition. The title may be anything the", - "author desires.", - "", - "The author-supplied title is also used as a [\"classes\"](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#classes) attribute value", - "after being converted into a valid identifier form (down-cased;", - "non-alphanumeric characters converted to single hyphens; \"admonition-\"", - "prefixed). For example, this admonition:", - "", - "```", - ".. admonition:: And, by the way...", - "", - " You can make up your own admonition too.", - "```", - "", - "becomes the following document tree (pseudo-XML):", - "", - "```", - "", - " ", - " ", - " And, by the way...", - " <paragraph>", - " You can make up your own admonition too.", - "```", - "", - "The [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) option overrides the computed [\"classes\"](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#classes) attribute", - "value." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#generic-admonition", - "license": "https://docutils.sourceforge.io/docs/" - }, - "hint(docutils.parsers.rst.directives.admonitions.Hint)": { - "is_markdown": true, - "description": [ - "## Specific Admonitions", - "", - "| | |", - "|-|-|", - "| Directive Types | \"attention\", \"caution\", \"danger\", \"error\", \"hint\",", - "\"important\", \"note\", \"tip\", \"warning\", \"admonition\" |", - "| Doctree Elements | attention, caution, danger, error, hint, important,", - "note, tip, warning, [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "Admonitions are specially marked \"topics\" that can appear anywhere an", - "ordinary body element can. They contain arbitrary body elements.", - "Typically, an admonition is rendered as an offset block in a document,", - "sometimes outlined or shaded, with a title matching the admonition", - "type. For example:", - "", - "```", - ".. DANGER::", - " Beware killer rabbits!", - "```", - "", - "This directive might be rendered something like this:", - "", - "```", - "+------------------------+", - "| !DANGER! |", - "| |", - "| Beware killer rabbits! |", - "+------------------------+", - "```", - "", - "The following admonition directives have been implemented:", - "- attention", - "- caution", - "- danger", - "- error", - "- hint", - "- important", - "- note", - "- tip", - "- warning", - "", - "Any text immediately following the directive indicator (on the same", - "line and/or indented on following lines) is interpreted as a directive", - "block and is parsed for normal body elements. For example, the", - "following \"note\" admonition directive contains one paragraph and a", - "bullet list consisting of two list items:", - "", - "```", - ".. note:: This is a note admonition.", - " This is the second line of the first paragraph.", - "", - " - The note contains all indented body elements", - " following.", - " - It includes this bullet list.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#hint", - "license": "https://docutils.sourceforge.io/docs/" - }, - "image(docutils.parsers.rst.directives.images.Image)": { - "is_markdown": true, - "description": [ - "## Image", - "", - "| | |", - "|-|-|", - "| Directive Type | \"image\" |", - "| Doctree Element | [image](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#image) |", - "| Directive Arguments | One, required (image URI). |", - "| Directive Options | Possible (see below). |", - "| Directive Content | None. |", - "", - "An \"image\" is a simple picture:", - "", - "```", - ".. image:: picture.png", - "```", - "", - "Inline images can be defined with an \"image\" directive in a [substitution", - "definition](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#substitution-definitions)", - "", - "The URI for the image source file is specified in the directive", - "argument. As with hyperlink targets, the image URI may begin on the", - "same line as the explicit markup start and target name, or it may", - "begin in an indented text block immediately following, with no", - "intervening blank lines. If there are multiple lines in the link", - "block, they are stripped of leading and trailing whitespace and joined", - "together.", - "", - "Optionally, the image link block may contain a flat field list, the", - "image options. For example:", - "", - "```", - ".. image:: picture.jpeg", - " :height: 100px", - " :width: 200 px", - " :scale: 50 %", - " :alt: alternate text", - " :align: right", - "```", - "", - "The following options are recognized:", - "", - "`alt`: text", - "Alternate text: a short description of the image, displayed by", - "applications that cannot display images, or spoken by applications", - "for visually impaired users.", - "", - "`height`: [length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units)", - "The desired height of the image.", - "Used to reserve space or scale the image vertically. When the \"scale\"", - "option is also specified, they are combined. For example, a height of", - "200px and a scale of 50 is equivalent to a height of 100px with no scale.", - "", - "`width`: [length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units) or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units) of the current line width", - "The width of the image.", - "Used to reserve space or scale the image horizontally. As with \"height\"", - "above, when the \"scale\" option is also specified, they are combined.", - "", - "`scale`: integer percentage (the \"%\" symbol is optional)", - "The uniform scaling factor of the image. The default is \"100\u00a0%\", i.e.", - "no scaling.", - "", - "If no \"height\" or \"width\" options are specified, the Python", - "Imaging Library (PIL/[Pillow](https://pypi.org/project/Pillow/)) may be used to determine them, if", - "it is installed and the image file is available.", - "", - "`align`: \"top\", \"middle\", \"bottom\", \"left\", \"center\", or \"right\"", - "The alignment of the image, equivalent to the HTML `<img>` tag's", - "deprecated \"align\" attribute or the corresponding \"vertical-align\" and", - "\"text-align\" CSS properties.", - "The values \"top\", \"middle\", and \"bottom\"", - "control an image's vertical alignment (relative to the text", - "baseline); they are only useful for inline images (substitutions).", - "The values \"left\", \"center\", and \"right\" control an image's", - "horizontal alignment, allowing the image to float and have the", - "text flow around it. The specific behavior depends upon the", - "browser or rendering software used.", - "", - "`target`: text (URI or reference name)", - "Makes the image into a hyperlink reference (\"clickable\"). The", - "option argument may be a URI (relative or absolute), or a", - "[reference name](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#reference-names) with underscore suffix (e.g. ``a name`_`).", - "", - "and the common options [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) and [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name)." - ], - "options": { - "alt": "text\nAlternate text: a short description of the image, displayed by\napplications that cannot display images, or spoken by applications\nfor visually impaired users.\n", - "height": "[length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units)\nThe desired height of the image.\nUsed to reserve space or scale the image vertically. When the \"scale\"\noption is also specified, they are combined. For example, a height of\n200px and a scale of 50 is equivalent to a height of 100px with no scale.\n", - "width": "[length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units) or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units) of the current line width\nThe width of the image.\nUsed to reserve space or scale the image horizontally. As with \"height\"\nabove, when the \"scale\" option is also specified, they are combined.\n", - "scale": "integer percentage (the \"%\" symbol is optional)\nThe uniform scaling factor of the image. The default is \"100\u00a0%\", i.e.\nno scaling.\n\nIf no \"height\" or \"width\" options are specified, the Python\nImaging Library (PIL/[Pillow](https://pypi.org/project/Pillow/)) may be used to determine them, if\nit is installed and the image file is available.\n", - "align": "\"top\", \"middle\", \"bottom\", \"left\", \"center\", or \"right\"\nThe alignment of the image, equivalent to the HTML `<img>` tag's\ndeprecated \"align\" attribute or the corresponding \"vertical-align\" and\n\"text-align\" CSS properties.\nThe values \"top\", \"middle\", and \"bottom\"\ncontrol an image's vertical alignment (relative to the text\nbaseline); they are only useful for inline images (substitutions).\nThe values \"left\", \"center\", and \"right\" control an image's\nhorizontal alignment, allowing the image to float and have the\ntext flow around it. The specific behavior depends upon the\nbrowser or rendering software used.\n", - "target": "text (URI or reference name)\nMakes the image into a hyperlink reference (\"clickable\"). The\noption argument may be a URI (relative or absolute), or a\n[reference name](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#reference-names) with underscore suffix (e.g. ``a name`_`).\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#image", - "license": "https://docutils.sourceforge.io/docs/" - }, - "important(docutils.parsers.rst.directives.admonitions.Important)": { - "is_markdown": true, - "description": [ - "## Specific Admonitions", - "", - "| | |", - "|-|-|", - "| Directive Types | \"attention\", \"caution\", \"danger\", \"error\", \"hint\",", - "\"important\", \"note\", \"tip\", \"warning\", \"admonition\" |", - "| Doctree Elements | attention, caution, danger, error, hint, important,", - "note, tip, warning, [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "Admonitions are specially marked \"topics\" that can appear anywhere an", - "ordinary body element can. They contain arbitrary body elements.", - "Typically, an admonition is rendered as an offset block in a document,", - "sometimes outlined or shaded, with a title matching the admonition", - "type. For example:", - "", - "```", - ".. DANGER::", - " Beware killer rabbits!", - "```", - "", - "This directive might be rendered something like this:", - "", - "```", - "+------------------------+", - "| !DANGER! |", - "| |", - "| Beware killer rabbits! |", - "+------------------------+", - "```", - "", - "The following admonition directives have been implemented:", - "- attention", - "- caution", - "- danger", - "- error", - "- hint", - "- important", - "- note", - "- tip", - "- warning", - "", - "Any text immediately following the directive indicator (on the same", - "line and/or indented on following lines) is interpreted as a directive", - "block and is parsed for normal body elements. For example, the", - "following \"note\" admonition directive contains one paragraph and a", - "bullet list consisting of two list items:", - "", - "```", - ".. note:: This is a note admonition.", - " This is the second line of the first paragraph.", - "", - " - The note contains all indented body elements", - " following.", - " - It includes this bullet list.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#important", - "license": "https://docutils.sourceforge.io/docs/" - }, - "include(docutils.parsers.rst.directives.misc.Include)": { - "is_markdown": true, - "description": [ - "## Including an External Document Fragment", - "", - "| | |", - "|-|-|", - "| Directive Type | \"include\" |", - "| Doctree Elements | Depend on data being included", - "([literal_block](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#literal-block) with `code` or `literal` option). |", - "| Directive Arguments | One, required (path to the file to include). |", - "| Directive Options | Possible (see below). |", - "| Directive Content | None. |", - "| Configuration Setting | [file_insertion_enabled](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#file-insertion-enabled) |", - "", - "The \"include\" directive represents a potential security hole. It", - "can be disabled with the \"[file_insertion_enabled](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#file-insertion-enabled)\" runtime setting.", - "", - "The \"include\" directive reads a text file. The directive argument is", - "the path to the file to be included, relative to the document containing", - "the directive. Unless the options `literal`, `code`, or `parser`", - "are given, the file is parsed in the current document's context at the", - "point of the directive. For example:", - "", - "```", - "This first example will be parsed at the document level, and can", - "thus contain any construct, including section headers.", - "", - ".. include:: inclusion.txt", - "", - "Back in the main document.", - "", - " This second example will be parsed in a block quote context.", - " Therefore it may only contain body elements. It may not", - " contain section headers.", - "", - " .. include:: inclusion.txt", - "```", - "", - "If an included document fragment contains section structure, the title", - "adornments must match those of the master document.", - "", - "Standard data files intended for inclusion in reStructuredText", - "documents are distributed with the Docutils source code, located in", - "the \"docutils\" package in the `docutils/parsers/rst/include`", - "directory. To access these files, use the special syntax for standard", - "\"include\" data files, angle brackets around the file name:", - "", - "```", - ".. include:: <isonum.txt>", - "```", - "", - "The current set of standard \"include\" data files consists of sets of", - "substitution definitions. See [reStructuredText Standard Definition", - "Files](https:/docutils.sourceforge.io/docs/ref/rst/definitions.html) for details.", - "", - "The following options are recognized:", - "", - "`start-line`: integer", - "Only the content starting from this line will be included.", - "(As usual in Python, the first line has index 0 and negative values", - "count from the end.)", - "", - "`end-line`: integer", - "Only the content up to (but excluding) this line will be included.", - "", - "`start-after`: text to find in the external data file", - "Only the content after the first occurrence of the specified text", - "will be included.", - "", - "`end-before`: text to find in the external data file", - "Only the content before the first occurrence of the specified text", - "(but after any `after` text) will be included.", - "", - "`parser`: parser name", - "Parse the included content with the specified parser.", - "(New in Docutils 0.17)", - "", - "`literal`: flag (empty)", - "The entire included text is inserted into the document as a single", - "literal block.", - "", - "`code`: [string] (formal language)", - "The argument and the included content are passed to", - "the [code](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#code) directive (useful for program listings).", - "", - "`number-lines`: [integer] (start line number)", - "Precede every code line with a line number.", - "The optional argument is the number of the first line (default 1).", - "Works only with `code` or `literal`.", - "", - "`encoding`: string", - "The text encoding of the external data file. Defaults to the", - "document's [input_encoding](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#input-encoding).", - "", - "`tab-width`: integer", - "Number of spaces for hard tab expansion.", - "A negative value prevents expansion of hard tabs. Defaults to the", - "[tab_width](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#tab-width) configuration setting.", - "", - "With `code` or `literal` the common options [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) and", - "[name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) are recognized as well.", - "", - "Combining `start/end-line` and `start-after/end-before` is possible. The", - "text markers will be searched in the specified lines (further limiting the", - "included content)." - ], - "options": { - "start-line": "integer\nOnly the content starting from this line will be included.\n(As usual in Python, the first line has index 0 and negative values\ncount from the end.)\n", - "end-line": "integer\nOnly the content up to (but excluding) this line will be included.\n", - "start-after": "text to find in the external data file\nOnly the content after the first occurrence of the specified text\nwill be included.\n", - "end-before": "text to find in the external data file\nOnly the content before the first occurrence of the specified text\n(but after any `after` text) will be included.\n", - "parser": "parser name\nParse the included content with the specified parser.\n(New in Docutils 0.17)\n", - "literal": "flag (empty)\nThe entire included text is inserted into the document as a single\nliteral block.\n", - "code": "[string] (formal language)\nThe argument and the included content are passed to\nthe [code](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#code) directive (useful for program listings).\n", - "number-lines": "[integer] (start line number)\nPrecede every code line with a line number.\nThe optional argument is the number of the first line (default 1).\nWorks only with `code` or `literal`.\n", - "encoding": "string\nThe text encoding of the external data file. Defaults to the\ndocument's [input_encoding](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#input-encoding).\n", - "tab-width": "integer\nNumber of spaces for hard tab expansion.\nA negative value prevents expansion of hard tabs. Defaults to the\n[tab_width](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#tab-width) configuration setting.\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#include", - "license": "https://docutils.sourceforge.io/docs/" - }, - "line-block(docutils.parsers.rst.directives.body.LineBlock)": { - "is_markdown": true, - "description": [ - "## Line Block", - "## Deprecated", - "", - "The \"line-block\" directive is deprecated. Use the [line block", - "syntax](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#line-blocks) instead.", - "", - "| | |", - "|-|-|", - "| Directive Type | \"line-block\" |", - "| Doctree Element | [line_block](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#line-block) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Becomes the body of the line block. |", - "", - "The \"line-block\" directive constructs an element where line breaks and", - "initial indentation is significant and inline markup is supported. It", - "is equivalent to a [parsed literal block](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#parsed-literal-block) with different rendering:", - "typically in an ordinary serif typeface instead of a", - "typewriter/monospaced face, and not automatically indented. (Have the", - "line-block directive begin a block quote to get an indented line", - "block.) Line blocks are useful for address blocks and verse (poetry,", - "song lyrics), where the structure of lines is significant. For", - "example, here's a classic:", - "", - "```", - "\"To Ma Own Beloved Lassie: A Poem on her 17th Birthday\", by", - "Ewan McTeagle (for Lassie O'Shea):", - "", - " .. line-block::", - "", - " Lend us a couple of bob till Thursday.", - " I'm absolutely skint.", - " But I'm expecting a postal order and I can pay you back", - " as soon as it comes.", - " Love, Ewan.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#line-block", - "license": "https://docutils.sourceforge.io/docs/" - }, - "list-table(docutils.parsers.rst.directives.tables.ListTable)": { - "is_markdown": true, - "description": [ - "## List Table", - "", - "| | |", - "|-|-|", - "| Directive Type | \"list-table\" |", - "| Doctree Element | [table](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#table) |", - "| Directive Arguments | One, optional (table title). |", - "| Directive Options | Possible (see below). |", - "| Directive Content | A uniform two-level bullet list. |", - "", - "(This is an initial implementation; [further ideas](https:/docutils.sourceforge.io/docs/ref/rst/../../dev/rst/alternatives.html#list-driven-tables) may be implemented", - "in the future.)", - "", - "The \"list-table\" directive is used to create a table from data in a", - "uniform two-level bullet list. \"Uniform\" means that each sublist", - "(second-level list) must contain the same number of list items.", - "", - "Example:", - "", - "```", - ".. list-table:: Frozen Delights!", - " :widths: 15 10 30", - " :header-rows: 1", - "", - " * - Treat", - " - Quantity", - " - Description", - " * - Albatross", - " - 2.99", - " - On a stick!", - " * - Crunchy Frog", - " - 1.49", - " - If we took the bones out, it wouldn't be", - " crunchy, now would it?", - " * - Gannet Ripple", - " - 1.99", - " - On a stick!", - "```", - "", - "The following options are recognized:", - "", - "`align`: \"left\", \"center\", or \"right\"", - "The horizontal alignment of the table.", - "(New in Docutils 0.13)", - "", - "`header-rows`: integer", - "The number of rows of list data to use in the table header.", - "Defaults to 0.", - "", - "`stub-columns`: integer", - "The number of table columns to use as stubs (row titles, on the", - "left). Defaults to 0.", - "", - "`width`: [length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units) or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units)", - "Sets the width of the table to the specified length or percentage", - "of the line width. If omitted, the renderer determines the width", - "of the table based on its contents or the column `widths`.", - "", - "`widths`: integer [integer...] or \"auto\"", - "A list of relative column widths.", - "The default is equal-width columns (100%/#columns).", - "", - "\"auto\" delegates the determination of column widths to the backend", - "(LaTeX, the HTML browser, ...).", - "", - "and the common options [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) and [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name)." - ], - "options": { - "align": "\"left\", \"center\", or \"right\"\nThe horizontal alignment of the table.\n(New in Docutils 0.13)\n", - "header-rows": "integer\nThe number of rows of list data to use in the table header.\nDefaults to 0.\n", - "stub-columns": "integer\nThe number of table columns to use as stubs (row titles, on the\nleft). Defaults to 0.\n", - "width": "[length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units) or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units)\nSets the width of the table to the specified length or percentage\nof the line width. If omitted, the renderer determines the width\nof the table based on its contents or the column `widths`.\n", - "widths": "integer [integer...] or \"auto\"\nA list of relative column widths.\nThe default is equal-width columns (100%/#columns).\n\n\"auto\" delegates the determination of column widths to the backend\n(LaTeX, the HTML browser, ...).\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#list-table", - "license": "https://docutils.sourceforge.io/docs/" - }, - "math(docutils.parsers.rst.directives.body.MathBlock)": { - "is_markdown": true, - "description": [ - "## Math", - "", - "| | |", - "|-|-|", - "| Directive Type | \"math\" |", - "| Doctree Element | [math_block](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#math-block) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Becomes the body of the math block.", - "(Content blocks separated by a blank line are put in", - "adjacent math blocks.) |", - "| Configuration Setting | [math_output](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#math-output) |", - "", - "The \"math\" directive inserts blocks with mathematical content", - "(display formulas, equations) into the document. The input format is", - "[LaTeX math syntax](https:/docutils.sourceforge.io/docs/ref/rst/../../ref/rst/mathematics.html) with support for Unicode symbols, for example:", - "", - "```", - ".. math::", - "", - " \u03b1_t(i) = P(O_1, O_2, \u2026 O_t, q_t = S_i \u03bb)", - "```", - "", - "Support is limited to a subset of LaTeX math by the conversion", - "required for many output formats. For HTML, the [math_output](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#math-output)", - "configuration setting (or the corresponding `--math-output`", - "command line option) select between alternative output formats with", - "different subsets of supported elements. If a writer does not", - "support math typesetting, the content is inserted verbatim.", - "", - "For inline formulas, use the [\"math\" role](https:/docutils.sourceforge.io/docs/ref/rst/roles.html#math)." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#math", - "license": "https://docutils.sourceforge.io/docs/" - }, - "meta(docutils.parsers.rst.directives.html.Meta)": { - "is_markdown": true, - "description": [ - "## Metadata", - "", - "| | |", - "|-|-|", - "| Directive Type | \"meta\" |", - "| Doctree Element | [meta](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#meta) |", - "| Directive Arguments | None. |", - "| Directive Options | None. |", - "| Directive Content | Must contain a flat field list. |", - "", - "The \"meta\" directive is used to specify metadata9 to be stored", - "in, e.g., [HTML meta elements](https://html.spec.whatwg.org/multipage/semantics.html#the-meta-element) or as [ODT file properties](https://en.wikipedia.org/wiki/OpenDocument_technical_specification#Metadata). The", - "LaTeX writer passes it to the `pdfinfo` option of the [hyperref](https://ctan.org/pkg/hyperref)", - "package. If an output format does not support \"invisible\" metadata,", - "content is silently dropped by the writer.", - "", - "Data from some [bibliographic fields](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#bibliographic-fields) is automatically", - "extracted and stored as metadata, too. However, Bibliographic", - "Fields are also displayed in the document's screen rendering or", - "printout.", - "", - "For an \"invisible\" document title, see the [metadata document", - "title](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#metadata-document-title) directive below.", - "", - "Within the directive block, a flat field list provides the syntax for", - "metadata. The field name becomes the contents of the \"name\" attribute", - "of the META tag, and the field body (interpreted as a single string", - "without inline markup) becomes the contents of the \"content\"", - "attribute. For example:", - "", - "```", - ".. meta::", - " :description: The reStructuredText plaintext markup language", - " :keywords: plaintext, markup language", - "```", - "", - "This would be converted to the following HTML:", - "", - "```", - "<meta name=\"description\"", - " content=\"The reStructuredText plaintext markup language\">", - "<meta name=\"keywords\" content=\"plaintext, markup language\">", - "```", - "", - "Support for other META attributes (\"http-equiv\", \"scheme\", \"lang\",", - "\"dir\") are provided through field arguments, which must be of the form", - "\"attr=value\":", - "", - "```", - ".. meta::", - " :description lang=en: An amusing story", - " :description lang=fr: Une histoire amusante", - "```", - "", - "And their HTML equivalents:", - "", - "```", - "<meta name=\"description\" lang=\"en\" content=\"An amusing story\">", - "<meta name=\"description\" lang=\"fr\" content=\"Une histoire amusante\">", - "```", - "", - "Some META tags use an \"http-equiv\" attribute instead of the \"name\"", - "attribute. To specify \"http-equiv\" META tags, simply omit the name:", - "", - "```", - ".. meta::", - " :http-equiv=Content-Type: text/html; charset=ISO-8859-1", - "```", - "", - "HTML equivalent:", - "", - "```", - "<meta http-equiv=\"Content-Type\"", - " content=\"text/html; charset=ISO-8859-1\">", - "```", - "9", - "\"Metadata\" is data about data, in this case data about the", - "document. Metadata is, e.g., used to describe and classify web", - "pages in the World Wide Web, in a form that is easy for search", - "engines to extract and collate." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#metadata", - "license": "https://docutils.sourceforge.io/docs/" - }, - "title(docutils.parsers.rst.directives.misc.Title)": { - "is_markdown": true, - "description": [ - "## Metadata Document Title", - "", - "| | |", - "|-|-|", - "| Directive Type | \"title\" |", - "| Doctree Element | Sets the document's [title attribute](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title-attribute). |", - "| Directive Arguments | One, required (the title text). |", - "| Directive Options | None. |", - "| Directive Content | None. |", - "", - "The \"title\" directive specifies the document title as metadata, which", - "does not become part of the document body. It overrides the", - "document-supplied [document title](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#document-title) and the [\"title\" configuration", - "setting](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#title). For example, in HTML output the metadata document title", - "appears in the title bar of the browser window." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#metadata-document-title", - "license": "https://docutils.sourceforge.io/docs/" - }, - "note(docutils.parsers.rst.directives.admonitions.Note)": { - "is_markdown": true, - "description": [ - "## Specific Admonitions", - "", - "| | |", - "|-|-|", - "| Directive Types | \"attention\", \"caution\", \"danger\", \"error\", \"hint\",", - "\"important\", \"note\", \"tip\", \"warning\", \"admonition\" |", - "| Doctree Elements | attention, caution, danger, error, hint, important,", - "note, tip, warning, [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "Admonitions are specially marked \"topics\" that can appear anywhere an", - "ordinary body element can. They contain arbitrary body elements.", - "Typically, an admonition is rendered as an offset block in a document,", - "sometimes outlined or shaded, with a title matching the admonition", - "type. For example:", - "", - "```", - ".. DANGER::", - " Beware killer rabbits!", - "```", - "", - "This directive might be rendered something like this:", - "", - "```", - "+------------------------+", - "| !DANGER! |", - "| |", - "| Beware killer rabbits! |", - "+------------------------+", - "```", - "", - "The following admonition directives have been implemented:", - "- attention", - "- caution", - "- danger", - "- error", - "- hint", - "- important", - "- note", - "- tip", - "- warning", - "", - "Any text immediately following the directive indicator (on the same", - "line and/or indented on following lines) is interpreted as a directive", - "block and is parsed for normal body elements. For example, the", - "following \"note\" admonition directive contains one paragraph and a", - "bullet list consisting of two list items:", - "", - "```", - ".. note:: This is a note admonition.", - " This is the second line of the first paragraph.", - "", - " - The note contains all indented body elements", - " following.", - " - It includes this bullet list.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#note", - "license": "https://docutils.sourceforge.io/docs/" - }, - "parsed-literal(docutils.parsers.rst.directives.body.ParsedLiteral)": { - "is_markdown": true, - "description": [ - "## Parsed Literal Block", - "", - "| | |", - "|-|-|", - "| Directive Type | \"parsed-literal\" |", - "| Doctree Element | [literal_block](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#literal-block) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Becomes the body of the literal block. |", - "", - "Unlike an ordinary literal block, the \"parsed-literal\" directive", - "constructs a literal block where the text is parsed for inline markup.", - "It is equivalent to a [line block](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#line-block) with different rendering:", - "typically in a typewriter/monospaced typeface, like an ordinary", - "literal block. Parsed literal blocks are useful for adding hyperlinks", - "to code examples.", - "", - "However, care must be taken with the text, because inline markup is", - "recognized and there is no protection from parsing. Backslash-escapes", - "may be necessary to prevent unintended parsing. And because the", - "markup characters are removed by the parser, care must also be taken", - "with vertical alignment. Parsed \"ASCII art\" is tricky, and extra", - "whitespace may be necessary.", - "", - "For example, all the element names in this content model are links:", - "", - "```", - ".. parsed-literal::", - "", - " ( (title_, subtitle_?)?,", - " decoration_?,", - " (docinfo_, transition_?)?,", - " `%structure.model;`_ )", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#parsed-literal", - "license": "https://docutils.sourceforge.io/docs/" - }, - "pull-quote(docutils.parsers.rst.directives.body.PullQuote)": { - "is_markdown": true, - "description": [ - "## Pull-Quote", - "", - "| | |", - "|-|-|", - "| Directive Type | \"pull-quote\" |", - "| Doctree Element | [block_quote](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#block-quote) |", - "| Directive Arguments | None. |", - "| Directive Options | None. |", - "| Directive Content | Interpreted as the body of the block quote. |", - "", - "A pull-quote is a small selection of text \"pulled out and quoted\",", - "typically in a larger typeface. Pull-quotes are used to attract", - "attention, especially in long articles.", - "", - "The \"pull-quote\" directive produces a \"pull-quote\"-class block quote.", - "See [Epigraph](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#epigraph) above for an analogous example." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#pull-quote", - "license": "https://docutils.sourceforge.io/docs/" - }, - "raw(docutils.parsers.rst.directives.misc.Raw)": { - "is_markdown": true, - "description": [ - "## Raw Data Pass-Through", - "", - "| | |", - "|-|-|", - "| Directive Type | \"raw\" |", - "| Doctree Element | [raw](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#raw) |", - "| Directive Arguments | One or more, required (output format types). |", - "| Directive Options | Possible (see below). |", - "| Directive Content | Stored verbatim, uninterpreted. None (empty) if a", - "\"file\" or \"url\" option given. |", - "| Configuration Setting | [raw_enabled](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#raw-enabled) |", - "", - "The \"raw\" directive represents a potential security hole. It can", - "be disabled with the \"[raw_enabled](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#raw-enabled)\" or \"[file_insertion_enabled](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#file-insertion-enabled)\"", - "runtime settings.", - "", - "The \"raw\" directive is a stop-gap measure allowing the author to", - "bypass reStructuredText's markup. It is a \"power-user\" feature", - "that should not be overused or abused. The use of \"raw\" ties", - "documents to specific output formats and makes them less portable.", - "", - "If you often need to use the \"raw\" directive or a \"raw\"-derived", - "interpreted text role, that is a sign either of overuse/abuse or", - "that functionality may be missing from reStructuredText. Please", - "describe your situation in a message to the [Docutils-users](https:/docutils.sourceforge.io/docs/ref/rst/../../user/mailing-lists.html#docutils-users) mailing", - "list.", - "", - "The \"raw\" directive indicates non-reStructuredText data that is to be", - "passed untouched to the Writer. The names of the output formats are", - "given in the directive arguments. The interpretation of the raw data", - "is up to the Writer. A Writer may ignore any raw output not matching", - "its format.", - "", - "For example, the following input would be passed untouched by an HTML", - "Writer:", - "", - "```", - ".. raw:: html", - "", - " <hr width=50 size=10>", - "```", - "", - "A LaTeX Writer could insert the following raw content into its", - "output stream:", - "", - "```", - ".. raw:: latex", - "", - " \\setlength{\\parindent}{0pt}", - "```", - "", - "Raw data can also be read from an external file, specified in a", - "directive option. In this case, the content block must be empty. For", - "example:", - "", - "```", - ".. raw:: html", - " :file: inclusion.html", - "```", - "", - "Inline equivalents of the \"raw\" directive can be defined via", - "[custom interpreted text roles](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#custom-interpreted-text-roles) derived from the [\"raw\" role](https:/docutils.sourceforge.io/docs/ref/rst/roles.html#raw).", - "", - "The following options are recognized:", - "", - "`file`: string (newlines removed)", - "The local filesystem path of a raw data file to be included.", - "", - "`url`: string (whitespace removed)", - "An Internet URL reference to a raw data file to be included.", - "", - "`encoding`: string", - "The text encoding of the external raw data (file or URL).", - "Defaults to the document's encoding (if specified).", - "", - "and the common option [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33)." - ], - "options": { - "file": "string (newlines removed)\nThe local filesystem path of a raw data file to be included.\n", - "url": "string (whitespace removed)\nAn Internet URL reference to a raw data file to be included.\n", - "encoding": "string\nThe text encoding of the external raw data (file or URL).\nDefaults to the document's encoding (if specified).\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#raw-directive", - "license": "https://docutils.sourceforge.io/docs/" - }, - "replace(docutils.parsers.rst.directives.misc.Replace)": { - "is_markdown": true, - "description": [ - "## Replacement Text", - "", - "| | |", - "|-|-|", - "| Directive Type | \"replace\" |", - "| Doctree Element | Text & [inline elements](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#inline-elements) |", - "| Directive Arguments | None. |", - "| Directive Options | None. |", - "| Directive Content | A single paragraph; may contain inline markup. |", - "", - "The \"replace\" directive is used to indicate replacement text for a", - "substitution reference. It may be used within [substitution", - "definitions](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#substitution-definitions) only. For example, this directive can be used to expand", - "abbreviations:", - "", - "```", - ".. |reST| replace:: reStructuredText", - "", - "Yes, |reST| is a long word, so I can't blame anyone for wanting to", - "abbreviate it.", - "```", - "", - "As reStructuredText doesn't support nested inline markup, the only way", - "to create a reference with styled text is to use substitutions with", - "the \"replace\" directive:", - "", - "```", - "I recommend you try |Python|_.", - "", - ".. |Python| replace:: Python, *the* best language around", - ".. _Python: https://www.python.org/", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#replace", - "license": "https://docutils.sourceforge.io/docs/" - }, - "role(docutils.parsers.rst.directives.misc.Role)": { - "is_markdown": true, - "description": [ - "## Custom Interpreted Text Roles", - "", - "| | |", - "|-|-|", - "| Directive Type | \"role\" |", - "| Doctree Element | None; affects subsequent parsing. |", - "| Directive Arguments | Two; one required (new [role name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#role-name)), one optional", - "(base role name, in parentheses). |", - "| Directive Options | Possible (depends on base role). |", - "| Directive Content | depends on base role. |", - "", - "The \"role\" directive dynamically creates a custom [interpreted text", - "role](https:/docutils.sourceforge.io/docs/ref/rst/roles.html) and registers it with the parser. This means that after", - "declaring a role like this:", - "", - "```", - ".. role:: custom", - "```", - "", - "the document may use the new \"custom\" role:", - "", - "```", - "An example of using :custom:`interpreted text`", - "```", - "", - "This will be parsed into the following document tree fragment:", - "", - "```", - "<paragraph>", - " An example of using", - " <inline classes=\"custom\">", - " interpreted text", - "```", - "", - "The role must be declared in a document before it can be used.", - "", - "Role names are case insensitive and must conform to the rules of", - "simple [reference names](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#reference-names) (but do not share a namespace with", - "hyperlinks, footnotes, and citations).", - "", - "The new role may be based on an existing role, specified as a second", - "argument in parentheses (whitespace optional):", - "", - "```", - ".. role:: custom(emphasis)", - "", - ":custom:`text`", - "```", - "", - "The parsed result is as follows:", - "", - "```", - "<paragraph>", - " <emphasis classes=\"custom\">", - " text", - "```", - "", - "A special case is the [\"raw\" role](https:/docutils.sourceforge.io/docs/ref/rst/roles.html#raw): derived roles enable", - "inline [raw data pass-through](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#raw-data-pass-through), e.g.:", - "", - "```", - ".. role:: raw-role(raw)", - " :format: html latex", - "", - ":raw-role:`raw text`", - "```", - "", - "If no base role is explicitly specified, a generic custom role is", - "automatically used. Subsequent interpreted text will produce an", - "\"inline\" element with a [\"classes\"](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#classes) attribute, as in the first example", - "above.", - "", - "With most roles, the \":class:\" option can be used to set a \"classes\"", - "attribute that is different from the role name. For example:", - "", - "```", - ".. role:: custom", - " :class: special", - "", - ":custom:`interpreted text`", - "```", - "", - "This is the parsed result:", - "", - "```", - "<paragraph>", - " <inline classes=\"special\">", - " interpreted text", - "```", - "", - "The following option is recognized by the \"role\" directive for most", - "base roles:", - "", - "`class`: text", - "Set the [\"classes\"](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#classes) attribute value on the element produced", - "(`inline`, or element associated with a base class) when the", - "custom interpreted text role is used. If no directive options are", - "specified, a \"class\" option with the directive argument (role", - "name) as the value is implied. See the [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) directive above.", - "", - "Specific base roles may support other options and/or directive", - "content. See the [reStructuredText Interpreted Text Roles](https:/docutils.sourceforge.io/docs/ref/rst/roles.html) document", - "for details." - ], - "options": { - "class": "text\nSet the [\"classes\"](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#classes) attribute value on the element produced\n(`inline`, or element associated with a base class) when the\ncustom interpreted text role is used. If no directive options are\nspecified, a \"class\" option with the directive argument (role\nname) as the value is implied. See the [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) directive above.\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#role", - "license": "https://docutils.sourceforge.io/docs/" - }, - "rubric(docutils.parsers.rst.directives.body.Rubric)": { - "is_markdown": true, - "description": [ - "## Rubric", - "", - "| | |", - "|-|-|", - "| Directive Type | \"rubric\" |", - "| Doctree Element | [rubric](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#rubric) |", - "| Directive Arguments | One, required (rubric text). |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | None. |", - "", - "rubric n. 1. a title, heading, or the like, in a manuscript,", - "book, statute, etc., written or printed in red or otherwise", - "distinguished from the rest of the text. ...", - "Random House Webster's College Dictionary, 1991", - "The \"rubric\" directive inserts a \"rubric\" element into the document", - "tree. A rubric is like an informal heading that doesn't correspond to", - "the document's structure." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#rubric", - "license": "https://docutils.sourceforge.io/docs/" - }, - "sidebar(docutils.parsers.rst.directives.body.Sidebar)": { - "is_markdown": true, - "description": [ - "## Sidebar", - "", - "| | |", - "|-|-|", - "| Directive Type | \"sidebar\" |", - "| Doctree Element | [sidebar](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#sidebar) |", - "| Directive Arguments | One, optional (sidebar title). |", - "| Directive Options | Possible (see below). |", - "| Directive Content | Interpreted as the sidebar body. |", - "", - "Sidebars are like miniature, parallel documents that occur inside", - "other documents, providing related or reference material. A sidebar", - "is typically offset by a border and \"floats\" to the side of the page;", - "the document's main text may flow around it. Sidebars can also be", - "likened to super-footnotes; their content is outside of the flow of", - "the document's main text.", - "", - "Sidebars may occur anywhere a section or transition may occur. Body", - "elements (including sidebars) may not contain nested sidebars.", - "", - "The directive's sole argument is interpreted as the sidebar title,", - "which may be followed by a subtitle option (see below); the next line", - "must be blank. All subsequent lines make up the sidebar body,", - "interpreted as body elements. For example:", - "", - "```", - ".. sidebar:: Optional Sidebar Title", - " :subtitle: Optional Sidebar Subtitle", - "", - " Subsequent indented lines comprise", - " the body of the sidebar, and are", - " interpreted as body elements.", - "```", - "", - "The following options are recognized:", - "", - "`subtitle`: text", - "The sidebar's subtitle.", - "", - "and the common options [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) and [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name)." - ], - "options": { - "subtitle": "text\nThe sidebar's subtitle.\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#sidebar", - "license": "https://docutils.sourceforge.io/docs/" - }, - "table(docutils.parsers.rst.directives.tables.RSTTable)": { - "is_markdown": true, - "description": [ - "## Table", - "", - "| | |", - "|-|-|", - "| Directive Type | \"table\" |", - "| Doctree Element | [table](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#table) |", - "| Directive Arguments | One, optional (table title). |", - "| Directive Options | Possible (see below). |", - "| Directive Content | A normal [reStructuredText table](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#tables). |", - "", - "The \"table\" directive is used to associate a", - "title with a table or specify options, e.g.:", - "", - "```", - ".. table:: Truth table for \"not\"", - " :widths: auto", - "", - " ===== =====", - " A not A", - " ===== =====", - " False True", - " True False", - " ===== =====", - "```", - "", - "The following options are recognized:", - "", - "`align`: \"left\", \"center\", or \"right\"", - "The horizontal alignment of the table (new in Docutils 0.13).", - "", - "`width`: [length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units) or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units)", - "Sets the width of the table to the specified length or percentage", - "of the line width. If omitted, the renderer determines the width", - "of the table based on its contents or the column `widths`.", - "", - "`widths`: \"auto\", \"grid\", or a list of integers", - "A list of relative column widths.", - "The default is the width of the input columns (in characters).", - "", - "\"auto\" delegates the determination of column widths to the backend", - "(LaTeX, the HTML browser, ...).", - "", - "\"grid\" restores the default, overriding a [table_style](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#table-style) or class", - "value \"colwidths-auto\".", - "", - "Plus the common options [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33) and [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name)." - ], - "options": { - "align": "\"left\", \"center\", or \"right\"\nThe horizontal alignment of the table (new in Docutils 0.13).\n", - "width": "[length](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#length-units) or [percentage](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#percentage-units)\nSets the width of the table to the specified length or percentage\nof the line width. If omitted, the renderer determines the width\nof the table based on its contents or the column `widths`.\n", - "widths": "\"auto\", \"grid\", or a list of integers\nA list of relative column widths.\nThe default is the width of the input columns (in characters).\n\n\"auto\" delegates the determination of column widths to the backend\n(LaTeX, the HTML browser, ...).\n\n\"grid\" restores the default, overriding a [table_style](https:/docutils.sourceforge.io/docs/ref/rst/../../user/config.html#table-style) or class\nvalue \"colwidths-auto\".\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#table", - "license": "https://docutils.sourceforge.io/docs/" - }, - "tip(docutils.parsers.rst.directives.admonitions.Tip)": { - "is_markdown": true, - "description": [ - "## Specific Admonitions", - "", - "| | |", - "|-|-|", - "| Directive Types | \"attention\", \"caution\", \"danger\", \"error\", \"hint\",", - "\"important\", \"note\", \"tip\", \"warning\", \"admonition\" |", - "| Doctree Elements | attention, caution, danger, error, hint, important,", - "note, tip, warning, [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "Admonitions are specially marked \"topics\" that can appear anywhere an", - "ordinary body element can. They contain arbitrary body elements.", - "Typically, an admonition is rendered as an offset block in a document,", - "sometimes outlined or shaded, with a title matching the admonition", - "type. For example:", - "", - "```", - ".. DANGER::", - " Beware killer rabbits!", - "```", - "", - "This directive might be rendered something like this:", - "", - "```", - "+------------------------+", - "| !DANGER! |", - "| |", - "| Beware killer rabbits! |", - "+------------------------+", - "```", - "", - "The following admonition directives have been implemented:", - "- attention", - "- caution", - "- danger", - "- error", - "- hint", - "- important", - "- note", - "- tip", - "- warning", - "", - "Any text immediately following the directive indicator (on the same", - "line and/or indented on following lines) is interpreted as a directive", - "block and is parsed for normal body elements. For example, the", - "following \"note\" admonition directive contains one paragraph and a", - "bullet list consisting of two list items:", - "", - "```", - ".. note:: This is a note admonition.", - " This is the second line of the first paragraph.", - "", - " - The note contains all indented body elements", - " following.", - " - It includes this bullet list.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#tip", - "license": "https://docutils.sourceforge.io/docs/" - }, - "topic(docutils.parsers.rst.directives.body.Topic)": { - "is_markdown": true, - "description": [ - "## Topic", - "", - "| | |", - "|-|-|", - "| Directive Type | \"topic\" |", - "| Doctree Element | [topic](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#topic) |", - "| Directive Arguments | One, required (topic title). |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as the topic body. |", - "", - "A topic is like a block quote with a title, or a self-contained", - "section with no subsections. Use the \"topic\" directive to indicate a", - "self-contained idea that is separate from the flow of the document.", - "Topics may occur anywhere a section or transition may occur. Body", - "elements and topics may not contain nested topics.", - "", - "The directive's sole argument is interpreted as the topic title; the", - "next line must be blank. All subsequent lines make up the topic body,", - "interpreted as body elements. For example:", - "", - "```", - ".. topic:: Topic Title", - "", - " Subsequent indented lines comprise", - " the body of the topic, and are", - " interpreted as body elements.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#topic", - "license": "https://docutils.sourceforge.io/docs/" - }, - "unicode(docutils.parsers.rst.directives.misc.Unicode)": { - "is_markdown": true, - "description": [ - "## Unicode Character Codes", - "", - "| | |", - "|-|-|", - "| Directive Type | \"unicode\" |", - "| Doctree Element | Text |", - "| Directive Arguments | One or more, required (Unicode character codes,", - "optional text, and comments). |", - "| Directive Options | Possible (see below). |", - "| Directive Content | None. |", - "", - "The \"unicode\" directive converts Unicode character codes (numerical", - "values) to characters, and may be used in [substitution definitions](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#substitution-definitions)", - "only.", - "", - "The arguments, separated by spaces, can be:", - "- character codes as", - "- decimal numbers or", - "- hexadecimal numbers, prefixed by `0x`, `x`, `\\x`, `U+`,", - "`u`, or `\\u` or as XML-style hexadecimal character entities,", - "e.g. `ᨫ`", - "- text, which is used as-is.", - "", - "Text following \" .. \" is a comment and is ignored. The spaces between", - "the arguments are ignored and thus do not appear in the output.", - "Hexadecimal codes are case-insensitive.", - "", - "For example, the following text:", - "", - "```", - "Copyright |copy| 2003, |BogusMegaCorp (TM)| |---|", - "all rights reserved.", - "", - ".. |copy| unicode:: 0xA9 .. copyright sign", - ".. |BogusMegaCorp (TM)| unicode:: BogusMegaCorp U+2122", - " .. with trademark sign", - ".. |---| unicode:: U+02014 .. em dash", - " :trim:", - "```", - "", - "results in:", - "", - "Copyright \u00a9 2003, BogusMegaCorp\u2122\u2014all rights reserved.", - "\u00a9BogusMegaCorp\u2122\u2014", - "The following options are recognized:", - "", - "`ltrim`: flag (empty)", - "Whitespace to the left of the substitution reference is removed.", - "", - "`rtrim`: flag (empty)", - "Whitespace to the right of the substitution reference is removed.", - "", - "`trim`: flag (empty)", - "Equivalent to `ltrim` plus `rtrim`; whitespace on both sides", - "of the substitution reference is removed." - ], - "options": { - "ltrim": "flag (empty)\nWhitespace to the left of the substitution reference is removed.\n", - "rtrim": "flag (empty)\nWhitespace to the right of the substitution reference is removed.\n", - "trim": "flag (empty)\nEquivalent to `ltrim` plus `rtrim`; whitespace on both sides\nof the substitution reference is removed.\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#unicode", - "license": "https://docutils.sourceforge.io/docs/" - }, - "warning(docutils.parsers.rst.directives.admonitions.Warning)": { - "is_markdown": true, - "description": [ - "## Specific Admonitions", - "", - "| | |", - "|-|-|", - "| Directive Types | \"attention\", \"caution\", \"danger\", \"error\", \"hint\",", - "\"important\", \"note\", \"tip\", \"warning\", \"admonition\" |", - "| Doctree Elements | attention, caution, danger, error, hint, important,", - "note, tip, warning, [admonition](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#admonition), [title](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#title) |", - "| Directive Arguments | None. |", - "| Directive Options | [class](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#id33), [name](https://docutils.sourceforge.io/docs/ref/rst/directives.txt#name) |", - "| Directive Content | Interpreted as body elements. |", - "", - "Admonitions are specially marked \"topics\" that can appear anywhere an", - "ordinary body element can. They contain arbitrary body elements.", - "Typically, an admonition is rendered as an offset block in a document,", - "sometimes outlined or shaded, with a title matching the admonition", - "type. For example:", - "", - "```", - ".. DANGER::", - " Beware killer rabbits!", - "```", - "", - "This directive might be rendered something like this:", - "", - "```", - "+------------------------+", - "| !DANGER! |", - "| |", - "| Beware killer rabbits! |", - "+------------------------+", - "```", - "", - "The following admonition directives have been implemented:", - "- attention", - "- caution", - "- danger", - "- error", - "- hint", - "- important", - "- note", - "- tip", - "- warning", - "", - "Any text immediately following the directive indicator (on the same", - "line and/or indented on following lines) is interpreted as a directive", - "block and is parsed for normal body elements. For example, the", - "following \"note\" admonition directive contains one paragraph and a", - "bullet list consisting of two list items:", - "", - "```", - ".. note:: This is a note admonition.", - " This is the second line of the first paragraph.", - "", - " - The note contains all indented body elements", - " following.", - " - It includes this bullet list.", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/directives.html#warning", - "license": "https://docutils.sourceforge.io/docs/" - } -} diff --git a/lib/esbonio/esbonio/lsp/rst/directives.py b/lib/esbonio/esbonio/lsp/rst/directives.py deleted file mode 100644 index 9b1b9e61d..000000000 --- a/lib/esbonio/esbonio/lsp/rst/directives.py +++ /dev/null @@ -1,76 +0,0 @@ -import importlib -import json -from typing import Dict -from typing import Optional -from typing import Tuple -from typing import Type -from typing import Union - -from docutils.parsers.rst import Directive -from docutils.parsers.rst import directives as docutils_directives - -from esbonio.lsp.directives import DirectiveLanguageFeature -from esbonio.lsp.directives import Directives -from esbonio.lsp.util import resources - - -class Docutils(DirectiveLanguageFeature): - """Support for docutils' built-in directives.""" - - def __init__(self) -> None: - self._directives: Optional[Dict[str, Type[Directive]]] = None - """Cache for known directives.""" - - @property - def directives(self) -> Dict[str, Type[Directive]]: - if self._directives is not None: - return self._directives - - ignored_directives = ["restructuredtext-test-directive"] - found_directives = { - **docutils_directives._directive_registry, - **docutils_directives._directives, - } - - self._directives = { - k: resolve_directive(v) - for k, v in found_directives.items() - if k not in ignored_directives - } - - return self._directives - - def get_implementation(self, directive: str, domain: Optional[str]): - if domain: - return None - - return self.directives.get(directive, None) - - def index_directives(self) -> Dict[str, Type[Directive]]: - return self.directives - - -def resolve_directive( - directive: Union[Type[Directive], Tuple[str, str]] -) -> Type[Directive]: - """Return the directive based on the given reference. - - 'Core' docutils directives are returned as tuples ``(modulename, ClassName)`` - so they need to be resolved manually. - """ - - if isinstance(directive, tuple): - mod, cls = directive - - modulename = f"docutils.parsers.rst.directives.{mod}" - module = importlib.import_module(modulename) - return getattr(module, cls) - - return directive - - -def esbonio_setup(directives: Directives): - documentation = resources.read_string("esbonio.lsp.rst", "directives.json") - - directives.add_documentation(json.loads(documentation)) - directives.add_feature(Docutils()) diff --git a/lib/esbonio/esbonio/lsp/rst/io.py b/lib/esbonio/esbonio/lsp/rst/io.py deleted file mode 100644 index 054438940..000000000 --- a/lib/esbonio/esbonio/lsp/rst/io.py +++ /dev/null @@ -1,222 +0,0 @@ -import logging -import typing -from typing import IO -from typing import Any -from typing import Callable -from typing import Optional -from typing import Type - -import pygls.uris as uri -from docutils import nodes -from docutils.core import Publisher -from docutils.io import NullOutput -from docutils.io import StringInput -from docutils.parsers.rst import Directive -from docutils.parsers.rst import Parser -from docutils.parsers.rst import directives -from docutils.parsers.rst import roles -from docutils.readers.standalone import Reader -from docutils.utils import Reporter -from docutils.writers import Writer -from pygls.workspace import Document -from sphinx.environment import default_settings - -from esbonio.lsp.util.patterns import DIRECTIVE -from esbonio.lsp.util.patterns import ROLE - - -class a_directive(nodes.Element, nodes.Inline): - """Represents a directive.""" - - -class a_role(nodes.Element): - """Represents a role.""" - - -def dummy_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - node = a_role() - node.line = lineno - - match = ROLE.match(rawtext) - if match is None: - node.attributes["text"] = rawtext - else: - node.attributes.update(match.groupdict()) - node.attributes["text"] = match.group(0) - - return [node], [] - - -class DummyDirective(Directive): - has_content = True - - def run(self): - node = a_directive() - node.line = self.lineno - parent = self.state.parent - lines = self.block_text - - # substitution definitions require special handling - if isinstance(parent, nodes.substitution_definition): - lines = parent.rawsource - - text = lines.split("\n")[0] - match = DIRECTIVE.match(text) - if match: - node.attributes.update(match.groupdict()) - node.attributes["text"] = match.group(0) - else: - self.state.reporter.warning(f"Unable to parse directive: '{text}'") - node.attributes["text"] = text - - if self.content: - # This is essentially what `nested_parse_with_titles` does in Sphinx. - # But by passing the content_offset to state.nested_parse we ensure any line - # numbers remain relative to the start of the current file. - current_titles = self.state.memo.title_styles - current_sections = self.state.memo.section_level - self.state.memo.title_styles = [] - self.state.memo.section_level = 0 - try: - self.state.nested_parse( - self.content, self.content_offset, node, match_titles=1 - ) - finally: - self.state.memo.title_styles = current_titles - self.state.memo.section_level = current_sections - - return [node] - - -class disable_roles_and_directives: - """Disables all roles and directives from being expanded. - - The ``CustomReSTDispactcher`` from Sphinx is *very* cool. - It provides a way to override the mechanism used to lookup roles and directives - during parsing! - - It's perfect for temporarily replacing all role and directive implementations with - dummy ones. Parsing an rst document with these dummy implementations effectively - gives us something that could be called an abstract syntax tree. - - Unfortunately, it's only available in a relatively recent version of Sphinx (4.4) - so we have to implement the same idea ourselves for now. - """ - - def __init__(self) -> None: - self.directive_backup: Optional[Callable] = None - self.role_backup: Optional[Callable] = None - - def __enter__(self) -> None: - self.directive_backup = directives.directive - self.role_backup = roles.role - - directives.directive = self.directive - roles.role = self.role - - def __exit__(self, exc_type: Type[Exception], exc_value: Exception, traceback: Any): - directives.directive = self.directive_backup # type: ignore - roles.role = self.role_backup # type: ignore - - self.directive_backup = None - self.role_backup = None - - def directive(self, directive_name, language_module, document): - return DummyDirective, [] - - def role(self, role_name, language_module, lineno, reporter): - return dummy_role, [] - - -class DummyWriter(Writer): - """A writer that doesn't do anything.""" - - def translate(self): - pass - - -class LogStream: - def __init__(self, logger: logging.Logger): - self.logger = logger - - def write(self, text: str): - self.logger.debug(text) - - -class LogReporter(Reporter): - """A docutils reporter that writes to the given logger.""" - - def __init__( - self, - logger: logging.Logger, - source: str, - report_level: int, - halt_level: int, - debug: bool, - error_handler: str, - ) -> None: - stream = typing.cast(IO, LogStream(logger)) - super().__init__( - source, report_level, halt_level, stream, debug, error_handler=error_handler - ) # type: ignore - - -class InitialDoctreeReader(Reader): - """A reader that replaces the default reporter with one compatible with esbonio's - logging setup.""" - - def __init__(self, logger: logging.Logger, *args, **kwargs): - self.logger = logger - super().__init__(*args, **kwargs) - - def new_document(self) -> nodes.document: - document = super().new_document() - - reporter = document.reporter - document.reporter = LogReporter( - self.logger, - reporter.source, - reporter.report_level, - reporter.halt_level, - reporter.debug_flag, - reporter.error_handler, - ) - - return document - - -def read_initial_doctree( - document: Document, logger: logging.Logger -) -> Optional[nodes.document]: - """Parse the given reStructuredText document into its "initial" doctree. - - An "initial" doctree can be thought of as the abstract syntax tree of a - reStructuredText document. This method disables all role and directives - from being executed, instead they are replaced with nodes that simply - represent that they exist. - - Parameters - ---------- - document - The document containing the reStructuredText source. - - logger - Logger to log debug info to. - """ - - parser = Parser() - with disable_roles_and_directives(): - publisher = Publisher( - reader=InitialDoctreeReader(logger), - parser=parser, - writer=DummyWriter(), - source_class=StringInput, - destination=NullOutput(), - ) - publisher.process_programmatic_settings(None, default_settings, None) - publisher.set_source( - source=document.source, source_path=uri.to_fs_path(document.uri) - ) - publisher.publish() - - return publisher.document diff --git a/lib/esbonio/esbonio/lsp/rst/roles.json b/lib/esbonio/esbonio/lsp/rst/roles.json deleted file mode 100644 index 23b69b1a8..000000000 --- a/lib/esbonio/esbonio/lsp/rst/roles.json +++ /dev/null @@ -1,448 +0,0 @@ -{ - "code(docutils.parsers.rst.roles.code_role)": { - "is_markdown": true, - "description": [ - "## `:code:`", - "", - "| | |", - "|-|-|", - "| Aliases | None |", - "| DTD Element | literal |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class), language |", - "| Content | None. |", - " |", - "", - "(New in Docutils 0.9.)", - "", - "The `code` role marks its content as code in a formal language.", - "", - "For syntax highlight of inline code, the [\"role\" directive](https:/docutils.sourceforge.io/docs/ref/rst/directives.html#role) can be used to", - "build custom roles with the code language specified in the \"language\"", - "option.", - "", - "For example, the following creates a LaTeX-specific \"latex\" role:", - "", - "```", - ".. role:: latex(code)", - " :language: latex", - "```", - "", - "Content of the new role is parsed and tagged by the [Pygments](https://pygments.org/) syntax", - "highlighter. See the [code directive](https:/docutils.sourceforge.io/docs/ref/rst/directives.html#code) for more info on parsing and display", - "of code in reStructuredText.", - "", - "In addition to \"[class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class)\", the following option is recognized:", - "", - "`language`: text", - "Name of the code's language.", - "See [supported languages and markup formats](https://pygments.org/languages/) for recognized values." - ], - "options": { - "language": "text\nName of the code's language.\nSee [supported languages and markup formats](https://pygments.org/languages/) for recognized values.\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#code", - "license": "https://docutils.sourceforge.io/docs/" - }, - "emphasis(docutils.parsers.rst.roles.GenericRole)": { - "is_markdown": true, - "description": [ - "## `:emphasis:`", - "", - "| | |", - "|-|-|", - "| Aliases | None |", - "| DTD Element | emphasis |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class). |", - "| Content | None. |", - " |", - "", - "Implements emphasis. These are equivalent:", - "", - "```", - "*text*", - ":emphasis:`text`", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#emphasis", - "license": "https://docutils.sourceforge.io/docs/" - }, - "literal(docutils.parsers.rst.roles.GenericRole)": { - "is_markdown": true, - "description": [ - "## `:literal:`", - "", - "| | |", - "|-|-|", - "| Aliases | None |", - "| DTD Element | literal |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class). |", - "| Content | None. |", - " |", - "", - "Implements inline literal text. These are equivalent:", - "", - "```", - "``text``", - ":literal:`text`", - "```", - "", - "Care must be taken with backslash-escapes though. These are not", - "equivalent:", - "", - "```", - "``text \\ and \\ backslashes``", - ":literal:`text \\ and \\ backslashes`", - "```", - "", - "The backslashes in the first line are preserved (and do nothing),", - "whereas the backslashes in the second line escape the following", - "spaces." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#literal", - "license": "https://docutils.sourceforge.io/docs/" - }, - "math(docutils.parsers.rst.roles.math_role)": { - "is_markdown": true, - "description": [ - "## `:math:`", - "", - "| | |", - "|-|-|", - "| Aliases | None |", - "| DTD Element | math |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class) |", - "| Content | None. |", - " |", - "", - "(New in Docutils 0.8.)", - "", - "The `math` role marks its content as mathematical notation (inline", - "formula).", - "", - "The input format is LaTeX math syntax without the \u201cmath delimiters\u201c", - "(`$ $`), for example:", - "", - "```", - "The area of a circle is :math:`A_\\text{c} = (\\pi/4) d^2`.", - "```", - "", - "See the [math directive](https:/docutils.sourceforge.io/docs/ref/rst/directives.html#math) (producing display formulas) for more info", - "on mathematical notation in reStructuredText." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#math", - "license": "https://docutils.sourceforge.io/docs/" - }, - "pep-reference(docutils.parsers.rst.roles.pep_reference_role)": { - "is_markdown": true, - "description": [ - "## `:pep-reference:`", - "", - "| | |", - "|-|-|", - "| Aliases | `:PEP:` |", - "| DTD Element | reference |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class). |", - "| Content | None. |", - " |", - "", - "The `:pep-reference:` role is used to create an HTTP reference to a", - "PEP (Python Enhancement Proposal). The `:PEP:` alias is usually", - "used. The content must be a number, for example:", - "", - "```", - "See :PEP:`287` for more information about reStructuredText.", - "```", - "", - "This is equivalent to:", - "", - "```", - "See `PEP 287`__ for more information about reStructuredText.", - "", - "__ https://www.python.org/dev/peps/pep-0287", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#pep-reference", - "license": "https://docutils.sourceforge.io/docs/" - }, - "raw(docutils.parsers.rst.roles.raw_role)": { - "is_markdown": true, - "description": [ - "## `raw`", - "", - "| | |", - "|-|-|", - "| Aliases | None |", - "| DTD Element | raw |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class), format |", - "| Content | None |", - " |", - "", - "The \"raw\" role is a stop-gap measure allowing the author to bypass", - "reStructuredText's markup. It is a \"power-user\" feature that", - "should not be overused or abused. The use of \"raw\" ties documents", - "to specific output formats and makes them less portable.", - "", - "If you often need to use \"raw\"-derived interpreted text roles or", - "the \"raw\" directive, that is a sign either of overuse/abuse or that", - "functionality may be missing from reStructuredText. Please", - "describe your situation in a message to the [Docutils-users](https:/docutils.sourceforge.io/docs/ref/rst/../../user/mailing-lists.html#docutils-user) mailing", - "list.", - "", - "The \"raw\" role indicates non-reStructuredText data that is to be", - "passed untouched to the Writer. It is the inline equivalent of the", - "[\"raw\" directive](https:/docutils.sourceforge.io/docs/ref/rst/directives.html#raw-directive); see its documentation for details on the", - "semantics.", - "", - "The \"raw\" role cannot be used directly. The [\"role\" directive](https:/docutils.sourceforge.io/docs/ref/rst/directives.html#role) must", - "first be used to build custom roles based on the \"raw\" role. One or", - "more formats (Writer names) must be provided in a \"format\" option.", - "", - "For example, the following creates an HTML-specific \"raw-html\" role:", - "", - "```", - ".. role:: raw-html(raw)", - " :format: html", - "```", - "", - "This role can now be used directly to pass data untouched to the HTML", - "Writer. For example:", - "", - "```", - "If there just *has* to be a line break here,", - ":raw-html:`<br />`", - "it can be accomplished with a \"raw\"-derived role.", - "But the line block syntax should be considered first.", - "```", - "", - "Roles based on \"raw\" should clearly indicate their origin, so", - "they are not mistaken for reStructuredText markup. Using a \"raw-\"", - "prefix for role names is recommended.", - "", - "In addition to \"[class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class)\", the following option is recognized:", - "", - "`format`: text", - "One or more space-separated output format names (Writer names)." - ], - "options": { - "format": "text\nOne or more space-separated output format names (Writer names).\n" - }, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#raw", - "license": "https://docutils.sourceforge.io/docs/" - }, - "rfc-reference(docutils.parsers.rst.roles.rfc_reference_role)": { - "is_markdown": true, - "description": [ - "## `:rfc-reference:`", - "", - "| | |", - "|-|-|", - "| Aliases | `:RFC:` |", - "| DTD Element | reference |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class). |", - "| Content | None. |", - " |", - "", - "The `:rfc-reference:` role is used to create an HTTP reference to an", - "RFC (Internet Request for Comments). The `:RFC:` alias is usually", - "used. The content must be a number 1, for example:", - "", - "```", - "See :RFC:`2822` for information about email headers.", - "```", - "", - "This is equivalent to:", - "", - "```", - "See `RFC 2822`__ for information about email headers.", - "", - "__ https://tools.ietf.org/html/rfc2822.html", - "```", - "1", - "You can link to a specific section by saying", - "`:rfc:`number#anchor``. (New in Docutils 0.15.)", - "", - "The anchor (anything following a `#`) is appended to", - "the reference without any checks and not shown in the link text.", - "", - "It is recommended to use [hyperlink references](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#hyperlink-references) for", - "anything more complex than a single RFC number." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#rfc-reference", - "license": "https://docutils.sourceforge.io/docs/" - }, - "strong(docutils.parsers.rst.roles.GenericRole)": { - "is_markdown": true, - "description": [ - "## `:strong:`", - "", - "| | |", - "|-|-|", - "| Aliases | None |", - "| DTD Element | strong |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class). |", - "| Content | None. |", - " |", - "", - "Implements strong emphasis. These are equivalent:", - "", - "```", - "**text**", - ":strong:`text`", - "```" - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#strong", - "license": "https://docutils.sourceforge.io/docs/" - }, - "subscript(docutils.parsers.rst.roles.GenericRole)": { - "is_markdown": true, - "description": [ - "## `:subscript:`", - "", - "| | |", - "|-|-|", - "| Aliases | `:sub:` |", - "| DTD Element | subscript |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class). |", - "| Content | None. |", - " |", - "", - "Implements subscripts.", - "", - "Whitespace or punctuation is required around interpreted text, but", - "often not desired with subscripts & superscripts.", - "Backslash-escaped whitespace can be used; the whitespace will be", - "removed from the processed document:", - "", - "```", - "H\\ :sub:`2`\\ O", - "E = mc\\ :sup:`2`", - "```", - "", - "In such cases, readability of the plain text can be greatly", - "improved with substitutions:", - "", - "```", - "The chemical formula for pure water is |H2O|.", - "", - ".. |H2O| replace:: H\\ :sub:`2`\\ O", - "```", - "", - "See [the reStructuredText spec](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html) for further information on", - "[character-level markup](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#character-level-inline-markup) and [the substitution mechanism](https:/docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#substitution-references)." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#subscript", - "license": "https://docutils.sourceforge.io/docs/" - }, - "superscript(docutils.parsers.rst.roles.GenericRole)": { - "is_markdown": true, - "description": [ - "## `:superscript:`", - "", - "| | |", - "|-|-|", - "| Aliases | `:sup:` |", - "| DTD Element | superscript |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class). |", - "| Content | None. |", - " |", - "", - "Implements superscripts. See the tip in [:subscript:](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#subscript) above." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#superscript", - "license": "https://docutils.sourceforge.io/docs/" - }, - "title-reference(docutils.parsers.rst.roles.GenericRole)": { - "is_markdown": true, - "description": [ - "## `:title-reference:`", - "", - "| | |", - "|-|-|", - "| Aliases | `:title:`, `:t:`. |", - "| DTD Element | title_reference |", - "| Customization | ", - "| | |", - "|-|-|", - "| Options | [class](https://docutils.sourceforge.io/docs/ref/rst/roles.txt#class). |", - "| Content | None. |", - " |", - "", - "The `:title-reference:` role is used to describe the titles of", - "books, periodicals, and other materials. It is the equivalent of the", - "HTML \"cite\" element, and it is expected that HTML writers will", - "typically render \"title_reference\" elements using \"cite\".", - "", - "Since title references are typically rendered with italics, they are", - "often marked up using `*emphasis*`, which is misleading and vague.", - "The \"title_reference\" element provides accurate and unambiguous", - "descriptive markup.", - "", - "Let's assume `:title-reference:` is the default interpreted text", - "role (see below) for this example:", - "", - "```", - "`Design Patterns` [GoF95]_ is an excellent read.", - "```", - "", - "The following document fragment ([pseudo-XML](https:/docutils.sourceforge.io/docs/ref/rst/../doctree.html#pseudo-xml)) will result from", - "processing:", - "", - "```", - "<paragraph>", - " <title_reference>", - " Design Patterns", - "", - " <citation_reference refname=\"gof95\">", - " GoF95", - " is an excellent read.", - "```", - "", - "`:title-reference:` is the default interpreted text role in the", - "standard reStructuredText parser. This means that no explicit role is", - "required. Applications of reStructuredText may designate a different", - "default role, in which case the explicit `:title-reference:` role", - "must be used to obtain a `title_reference` element." - ], - "options": {}, - "source": "https://docutils.sourceforge.io/docs/ref/rst/roles.html#title-reference", - "license": "https://docutils.sourceforge.io/docs/" - } -} diff --git a/lib/esbonio/esbonio/lsp/rst/roles.py b/lib/esbonio/esbonio/lsp/rst/roles.py deleted file mode 100644 index d36201da8..000000000 --- a/lib/esbonio/esbonio/lsp/rst/roles.py +++ /dev/null @@ -1,50 +0,0 @@ -import json -from typing import Any -from typing import Dict -from typing import Optional - -import docutils.parsers.rst.roles as docutils_roles - -from esbonio.lsp.roles import RoleLanguageFeature -from esbonio.lsp.roles import Roles -from esbonio.lsp.rst import RstLanguageServer -from esbonio.lsp.util import resources - - -class Docutils(RoleLanguageFeature): - """Support for docutils' built-in roles.""" - - def __init__(self) -> None: - self._roles: Optional[Dict[str, Any]] = None - """Cache for known roles.""" - - @property - def roles(self) -> Dict[str, Any]: - if self._roles is not None: - return self._roles - - found_roles = {**docutils_roles._roles, **docutils_roles._role_registry} - - self._roles = { - k: v - for k, v in found_roles.items() - if v != docutils_roles.unimplemented_role - } - - return self._roles - - def get_implementation(self, role: str, domain: str): - if domain: - return None - - return self.roles.get(role, None) - - def index_roles(self) -> Dict[str, Any]: - return self.roles - - -def esbonio_setup(rst: RstLanguageServer, roles: Roles): - documentation = resources.read_string("esbonio.lsp.rst", "roles.json") - - roles.add_documentation(json.loads(documentation)) - roles.add_feature(Docutils()) diff --git a/lib/esbonio/esbonio/lsp/sphinx/__init__.py b/lib/esbonio/esbonio/lsp/sphinx/__init__.py deleted file mode 100644 index 565f2114a..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/__init__.py +++ /dev/null @@ -1,851 +0,0 @@ -import json -import logging -import pathlib -import platform -import traceback -import typing -import warnings -from functools import partial -from multiprocessing import Process -from multiprocessing import Queue -from typing import IO -from typing import Any -from typing import Dict -from typing import Iterator -from typing import List -from typing import Optional -from typing import Tuple - -import pygls.uris as Uri -from lsprotocol.types import DeleteFilesParams -from lsprotocol.types import Diagnostic -from lsprotocol.types import DiagnosticSeverity -from lsprotocol.types import DidSaveTextDocumentParams -from lsprotocol.types import InitializedParams -from lsprotocol.types import InitializeParams -from lsprotocol.types import MessageType -from lsprotocol.types import Position -from lsprotocol.types import Range -from lsprotocol.types import ShowDocumentParams -from sphinx import __version__ as __sphinx_version__ -from sphinx.application import Sphinx -from sphinx.domains import Domain -from sphinx.errors import ConfigError -from sphinx.util import console -from sphinx.util import logging as sphinx_logging_module -from sphinx.util.logging import NAMESPACE as SPHINX_LOG_NAMESPACE -from sphinx.util.logging import VERBOSITY_MAP - -from esbonio.cli import setup_cli -from esbonio.lsp.rst import RstLanguageServer -from esbonio.lsp.sphinx.config import InitializationOptions -from esbonio.lsp.sphinx.config import MissingConfigError -from esbonio.lsp.sphinx.config import SphinxConfig -from esbonio.lsp.sphinx.config import SphinxLogHandler -from esbonio.lsp.sphinx.config import SphinxServerConfig -from esbonio.lsp.sphinx.preview import make_preview_server -from esbonio.lsp.sphinx.preview import start_preview_server - -from .line_number_transform import LineNumberTransform - -__all__ = [ - "InitializationOptions", - "MissingConfigError", - "SphinxConfig", - "SphinxServerConfig", - "SphinxLanguageServer", -] - -IS_LINUX = platform.system() == "Linux" - -# fmt: off -# Order matters! -DEFAULT_MODULES = [ - "esbonio.lsp.directives", # Generic directive support - "esbonio.lsp.roles", # Generic roles support - "esbonio.lsp.rst.directives", # docutils directives - "esbonio.lsp.rst.roles", # docutils roles - "esbonio.lsp.sphinx.autodoc", # automodule, autoclass, etc. - "esbonio.lsp.sphinx.codeblocks", # code-block, highlight, etc. - "esbonio.lsp.sphinx.domains", # Sphinx domains - "esbonio.lsp.sphinx.directives", # Sphinx directives - "esbonio.lsp.sphinx.images", # image, figure etc - "esbonio.lsp.sphinx.includes", # include, literal-include etc. - "esbonio.lsp.sphinx.roles", # misc roles added by Sphinx e.g. :download: -] -"""The modules to load in the default configuration of the server.""" -# fmt: on - - -class SphinxLanguageServer(RstLanguageServer): - """A language server dedicated to working with Sphinx projects.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.app: Optional[Sphinx] = None - """The Sphinx application instance.""" - - self.sphinx_args: Dict[str, Any] = {} - """The current Sphinx configuration will all variables expanded.""" - - self.sphinx_log: Optional[SphinxLogHandler] = None - """Logging handler for sphinx messages.""" - - self.preview_process: Optional[Process] = None - """The process hosting the preview server.""" - - self.preview_port: Optional[int] = None - """The port the preview server is running on.""" - - self._role_target_types: Optional[Dict[str, List[str]]] = None - """Cache for role target types.""" - - self.file_list_pending_build_version_updates: List[Tuple[str, int]] = [] - """List of all the files that need an updated last_build_version""" - - @property - def configuration(self) -> Dict[str, Any]: - """Return the server's actual configuration.""" - config = super().configuration - sphinx_config = SphinxConfig.from_arguments(sphinx_args=self.sphinx_args) - - if sphinx_config is None: - self.logger.error("Unable to determine SphinxConfig!") - return config - - if self.user_config is None: - self.logger.error("Unable to determine user config!") - return config - - # We always run Sphinx in "'-Q' mode", so we need to go back to the user's - # config to get those values. - sphinx_config.silent = self.user_config.sphinx.silent # type: ignore - sphinx_config.quiet = self.user_config.sphinx.quiet # type: ignore - - # 'Make mode' isn't something that can be inferred from Sphinx args either. - sphinx_config.make_mode = self.user_config.sphinx.make_mode # type: ignore - - config["sphinx"] = self.converter.unstructure(sphinx_config) - config["sphinx"]["command"] = ["sphinx-build"] + sphinx_config.to_cli_args() - config["sphinx"]["version"] = __sphinx_version__ - - config["server"] = self.converter.unstructure(self.user_config.server) - - return config - - def initialize(self, params: InitializeParams): - super().initialize(params) - self.user_config = self.converter.structure( - params.initialization_options or {}, InitializationOptions - ) - - def initialized(self, params: InitializedParams): - self.app = self._initialize_sphinx() - self.build() - - def _initialize_sphinx(self): - try: - return self.create_sphinx_app(self.user_config) # type: ignore - except MissingConfigError: - self.show_message( - message="Unable to find your 'conf.py', features that depend on Sphinx will be unavailable", - msg_type=MessageType.Warning, - ) - self.send_notification( - "esbonio/buildComplete", - { - "config": self.configuration, - "error": True, - "warnings": 0, - }, - ) - except Exception as exc: - self.logger.error(traceback.format_exc()) - uri, diagnostic = exception_to_diagnostic(exc) - self.set_diagnostics("conf.py", uri, [diagnostic]) - - self.sync_diagnostics() - self.send_notification( - "esbonio/buildComplete", - {"config": self.configuration, "error": True, "warnings": 0}, - ) - - def on_shutdown(self, *args): - if self.preview_process: - if not hasattr(self.preview_process, "kill"): - self.preview_process.terminate() - else: - self.preview_process.kill() - - def save(self, params: DidSaveTextDocumentParams): - super().save(params) - - filepath = Uri.to_fs_path(params.text_document.uri) - if filepath.endswith("conf.py"): - if self.app: - conf_dir = pathlib.Path(self.app.confdir) - else: - # The user's config is currently broken... where should their conf.py be? - if self.user_config is not None: - config = typing.cast(InitializationOptions, self.user_config).sphinx - else: - config = SphinxConfig() - - conf_dir = config.resolve_conf_dir( - self.workspace.root_uri - ) or pathlib.Path(".") - - if str(conf_dir / "conf.py") == filepath: - self.clear_diagnostics("conf.py") - self.sync_diagnostics() - self.app = self._initialize_sphinx() - - self.build() - - def delete_files(self, params: DeleteFilesParams): - self.logger.debug("Deleted files: %s", params.files) - - # Files don't exist anymore, so diagnostics must be cleared. - for file in params.files: - self.clear_diagnostics("sphinx", file.uri) - - self.build() - - def build( - self, force_all: bool = False, filenames: Optional[List[str]] = None - ) -> None: - """Trigger sphinx build. Force complete rebuild with flag or build only selected files in the list.""" - if not self.app: - return - - self.logger.debug("Building...") - self.send_notification("esbonio/buildStart", {}) - self.clear_diagnostics("sphinx-build") - self.sync_diagnostics() - - # Reset the warnings counter - self.app._warncount = 0 - error = False - - if self.sphinx_log is not None: - self.sphinx_log.diagnostics = {} - - try: - self.app.build(force_all, filenames) - except Exception as exc: - error = True - self.logger.error(traceback.format_exc()) - uri, diagnostic = exception_to_diagnostic(exc) - self.set_diagnostics("sphinx-build", uri, [diagnostic]) - - if self.sphinx_log is not None: - for doc, diagnostics in self.sphinx_log.diagnostics.items(): - self.logger.debug("Found %d problems for %s", len(diagnostics), doc) - self.set_diagnostics("sphinx", doc, diagnostics) - - self.sync_diagnostics() - - self.send_notification( - "esbonio/buildComplete", - { - "config": self.configuration, - "error": error, - "warnings": self.app._warncount, - }, - ) - - def cb_env_before_read_docs(self, app, env, docnames: List[str]): - """Callback handling env-before-read-docs event.""" - - # Determine if any unsaved files need to be added to the build list - if self.user_config.server.enable_live_preview: # type: ignore - is_building = set(docnames) - - for docname in env.found_docs - is_building: - filepath = env.doc2path(docname, base=True) - uri = Uri.from_fs_path(filepath) - - doc = self.workspace.get_document(uri) - current_version = doc.version or 0 - - last_build_version = getattr(doc, "last_build_version", 0) - if last_build_version < current_version: - docnames.append(docname) - - # Clear diagnostics for any to-be built files - for docname in docnames: - filepath = env.doc2path(docname, base=True) - uri = Uri.from_fs_path(filepath) - self.clear_diagnostics("sphinx", uri) - - doc = self.workspace.get_document(uri) - current_version = doc.version or 0 - self.file_list_pending_build_version_updates.append((uri, current_version)) # type: ignore - - def cb_build_finished(self, app, exception): - """Callback handling build-finished event.""" - if exception: - self.file_list_pending_build_version_updates = [] - return - - for uri, updated_version in self.file_list_pending_build_version_updates: - doc = self.workspace.get_document(uri) - last_build_version = getattr(doc, "last_build_version", 0) - if last_build_version < updated_version: - doc.last_build_version = updated_version # type: ignore - - self.file_list_pending_build_version_updates = [] - - def cb_source_read(self, app, docname, source): - """Callback handling source_read event.""" - - if not self.user_config.server.enable_live_preview: # type: ignore - return - - filepath = app.env.doc2path(docname, base=True) - uri = Uri.from_fs_path(filepath) - - doc = self.workspace.get_document(uri) - source[0] = doc.source - - def create_sphinx_app(self, options: InitializationOptions) -> Optional[Sphinx]: - """Create a Sphinx application instance with the given config.""" - sphinx = options.sphinx - server = options.server - self.logger.debug( - "User Config %s", json.dumps(self.converter.unstructure(sphinx), indent=2) - ) - - # Until true multi-root support can be implemented let's try each workspace - # folder and use the first valid configuration we can find. - for folder_uri in self.workspace.folders.keys(): - self.logger.debug("Workspace Folder: '%s'", folder_uri) - - try: - sphinx_config = sphinx.resolve(folder_uri) - break - except MissingConfigError: - self.logger.debug( - "No Sphinx conifg found in workspace folder: '%s'", folder_uri - ) - - # Not all clients use/support workspace folders, as a fallback, try the root_uri. - else: - self.logger.debug("Workspace root '%s'", self.workspace.root_uri) - sphinx_config = sphinx.resolve(self.workspace.root_uri) - - self.sphinx_args = sphinx_config.to_application_args() - self.logger.debug("Sphinx Args %s", json.dumps(self.sphinx_args, indent=2)) - - # Override Sphinx's logging setup with our own. - sphinx_logging_module.setup = partial(self._logging_setup, server, sphinx) - app = Sphinx(**self.sphinx_args) - - self._load_sphinx_extensions(app) - self._load_sphinx_config(app) - - if self.user_config.server.enable_scroll_sync: # type: ignore - app.add_transform(LineNumberTransform) - - app.connect("env-before-read-docs", self.cb_env_before_read_docs) - - if self.user_config.server.enable_live_preview: # type: ignore - app.connect("source-read", self.cb_source_read, priority=0) - app.connect("build-finished", self.cb_build_finished) - - return app - - def _logging_setup( - self, - server: SphinxServerConfig, - sphinx: SphinxConfig, - app: Sphinx, - status: IO, - warning: IO, - ): - # Disable color escape codes in Sphinx's log messages - console.nocolor() - - if not server.hide_sphinx_output and not sphinx.silent: - sphinx_logger = logging.getLogger(SPHINX_LOG_NAMESPACE) - - # Be sure to remove any old handlers. - for handler in sphinx_logger.handlers: - if isinstance(handler, SphinxLogHandler): - sphinx_logger.handlers.remove(handler) - - self.sphinx_log = SphinxLogHandler(app, self) - sphinx_logger.addHandler(self.sphinx_log) - - if sphinx.quiet: - level = logging.WARNING - else: - level = VERBOSITY_MAP[app.verbosity] - - sphinx_logger.setLevel(level) - self.sphinx_log.setLevel(level) - - formatter = logging.Formatter("%(message)s") - self.sphinx_log.setFormatter(formatter) - - def _load_sphinx_extensions(self, app: Sphinx): - """Loop through each of Sphinx's extensions and see if any contain server - functionality. - """ - - for name, ext in app.extensions.items(): - mod = ext.module - - setup = getattr(mod, "esbonio_setup", None) - if setup is None: - self.logger.debug( - "Skipping extension '%s', missing 'esbonio_setup' fuction", name - ) - continue - - self.load_extension(name, setup) - - def _load_sphinx_config(self, app: Sphinx): - """Try and load the config as an server extension.""" - - name = "<sphinx-config>" - - setup = app.config._raw_config.get("esbonio_setup", None) - if not setup or not callable(setup): - return - - self.load_extension(name, setup) - - def preview(self, options: Dict[str, Any]) -> Dict[str, Any]: - if not self.app or not self.app.builder: - return {} - - builder_name = self.app.builder.name - if builder_name not in {"html"}: - self.show_message( - f"Previews are not currently supported for the '{builder_name}' builder." - ) - - return {} - - if not self.preview_process and IS_LINUX: - self.logger.debug("Starting preview server.") - server = make_preview_server(self.app.outdir) # type: ignore[arg-type] - self.preview_port = server.server_port - - self.preview_process = Process(target=server.serve_forever, daemon=True) - self.preview_process.start() - - if not self.preview_process and not IS_LINUX: - self.logger.debug("Starting preview server") - - q: Queue = Queue() - self.preview_process = Process( - target=start_preview_server, args=(q, self.app.outdir), daemon=True - ) - self.preview_process.start() - self.preview_port = q.get() - - if options.get("show", True): - self.show_document( - ShowDocumentParams( - uri=f"http://localhost:{self.preview_port}", external=True - ) - ) - - return {"port": self.preview_port} - - def get_doctree( - self, *, docname: Optional[str] = None, uri: Optional[str] = None - ) -> Optional[Any]: - """Return the initial doctree corresponding to the specified document. - - The ``docname`` of a document is its path relative to the project's ``srcdir`` - minus the extension e.g. the docname of the file ``docs/lsp/features.rst`` - would be ``lsp/features``. - - Parameters - ---------- - docname: - Returns the doctree that corresponds with the given docname - uri: - Returns the doctree that corresponds with the given uri. - """ - - if self.app is None or self.app.env is None or self.app.builder is None: - return None - - if uri is not None: - fspath = Uri.to_fs_path(uri) - docname = self.app.env.path2doc(fspath) - - if docname is None: - return None - - try: - return self.app.env.get_and_resolve_doctree(docname, self.app.builder) - except FileNotFoundError: - self.logger.debug("Could not find doctree for '%s'", docname) - # self.logger.debug(traceback.format_exc()) - return None - - def get_domain(self, name: str) -> Optional[Domain]: - """Return the domain with the given name. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0`` - - If a domain with the given name cannot be found, this method will return None. - - Parameters - ---------- - name: - The name of the domain - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_domains() is deprecated and will be removed in v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - if self.app is None or self.app.env is None: - return None - - domains = self.app.env.domains - return domains.get(name, None) - - def get_domains(self) -> Iterator[Tuple[str, Domain]]: - """Get all the domains registered with an applications. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0`` - - Returns a generator that iterates through all of an application's domains, - taking into account configuration variables such as ``primary_domain``. - Yielded values will be a tuple of the form ``(prefix, domain)`` where - - - ``prefix`` is the namespace that should be used when referencing items - in the domain - - ``domain`` is the domain object itself. - - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_domains() is deprecated and will be removed in v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - if self.app is None or self.app.env is None: - return [] - - domains = self.app.env.domains - primary_domain = self.app.config.primary_domain - - for name, domain in domains.items(): - prefix = name - - # Items from the standard and primary domains don't require the namespace prefix - if name == "std" or name == primary_domain: - prefix = "" - - yield prefix, domain - - def get_directive_options(self, name: str) -> Dict[str, Any]: - """Return the options specification for the given directive. - - .. deprecated:: 0.14.2 - - This will be removed in ``v1.0`` - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_directive_options() is deprecated and will be removed in " - "v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - directive = self.get_directives().get(name, None) - if directive is None: - return {} - - options = directive.option_spec - - if name.startswith("auto") and self.app: - self.logger.debug("Processing options for '%s' directive", name) - name = name.replace("auto", "") - - self.logger.debug("Documenter name is '%s'", name) - documenter = self.app.registry.documenters.get(name, None) - - if documenter is not None: - options = documenter.option_spec - - return options or {} - - def get_default_role(self) -> Tuple[Optional[str], Optional[str]]: - """Return the project's default role""" - - if not self.app: - return None, None - - role = self.app.config.default_role - if not role: - return None, None - - if ":" in role: - domain, name = role.split(":") - - if domain == self.app.config.primary_domain: - domain = "" - - return domain, name - - return None, role - - def get_role_target_types(self, name: str, domain_name: str = "") -> List[str]: - """Return a map indicating which object types a role is capable of linking - with. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0`` - - For example - - .. code-block:: python - - { - "func": ["function"], - "class": ["class", "exception"] - } - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_role_target_types() is deprecated and will be removed in " - "v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - key = f"{domain_name}:{name}" if domain_name else name - - if self._role_target_types is not None: - return self._role_target_types.get(key, []) - - self._role_target_types = {} - - for prefix, domain in self.get_domains(): - fmt = "{prefix}:{name}" if prefix else "{name}" - - for name, item_type in domain.object_types.items(): - for role in item_type.roles: - role_key = fmt.format(name=role, prefix=prefix) - target_types = self._role_target_types.get(role_key, list()) - target_types.append(name) - - self._role_target_types[role_key] = target_types - - types = self._role_target_types.get(key, []) - self.logger.debug("Role '%s' targets object types '%s'", key, types) - - return types - - def get_role_targets(self, name: str, domain: str = "") -> List[tuple]: - """Return a list of objects targeted by the given role. - - Parameters - ---------- - name: - The name of the role - domain: - The domain the role is a part of, if applicable. - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_role_targets() is deprecated and will be removed in " - "v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - targets: List[tuple] = [] - domain_obj: Optional[Domain] = None - - if domain: - domain_obj = self.get_domain(domain) - else: - std = self.get_domain("std") - if std and name in std.roles: - domain_obj = std - - elif self.app and self.app.config.primary_domain: - domain_obj = self.get_domain(self.app.config.primary_domain) - - target_types = set(self.get_role_target_types(name, domain)) - - if not domain_obj: - self.logger.debug("Unable to find domain for role '%s:%s'", domain, name) - return [] - - for obj in domain_obj.get_objects(): - if obj[2] in target_types: - targets.append(obj) - - return targets - - def get_intersphinx_projects(self) -> List[str]: - """Return the list of configured intersphinx project names. - - .. deprecated:: 0.15.0 - - This will be removed in ``v.1.0`` - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_intersphinx_projects() is deprecated and will be removed in " - "v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - if self.app is None: - return [] - - inv = getattr(self.app.env, "intersphinx_named_inventory", {}) - return list(inv.keys()) - - def has_intersphinx_targets( - self, project: str, name: str, domain: str = "" - ) -> bool: - """Return ``True`` if the given intersphinx project has targets targeted by the - given role. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0`` - - Parameters - ---------- - project: - The project to check - name: - The name of the role - domain: - The domain the role is a part of, if applicable. - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.has_intersphinx_targets() is deprecated and will be removed in " - "v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - targets = self.get_intersphinx_targets(project, name, domain) - - if len(targets) == 0: - return False - - return any([len(items) > 0 for items in targets.values()]) - - def get_intersphinx_targets( - self, project: str, name: str, domain: str = "" - ) -> Dict[str, Dict[str, tuple]]: - """Return the intersphinx objects targeted by the given role. - - .. deprecated:: 0.15.0 - - This will be removed in ``v1.0`` - - Parameters - ---------- - project: - The project to return targets from - name: - The name of the role - domain: - The domain the role is a part of, if applicable. - """ - - clsname = self.__class__.__name__ - warnings.warn( - f"{clsname}.get_intersphinx_targets() is deprecated and will be removed in " - "v1.0.", - DeprecationWarning, - stacklevel=2, - ) - - if self.app is None: - return {} - - inv = getattr(self.app.env, "intersphinx_named_inventory", {}) - if project not in inv: - return {} - - targets = {} - inv = inv[project] - - for target_type in self.get_role_target_types(name, domain): - explicit_domain = f"{domain}:{target_type}" - if explicit_domain in inv: - targets[target_type] = inv[explicit_domain] - continue - - primary_domain = f'{self.app.config.primary_domain or ""}:{target_type}' - if primary_domain in inv: - targets[target_type] = inv[primary_domain] - continue - - std_domain = f"std:{target_type}" - if std_domain in inv: - targets[target_type] = inv[std_domain] - - return targets - - -def exception_to_diagnostic(exc: BaseException): - """Convert an exception into a diagnostic we can send to the client.""" - - # Config errors sometimes wrap the true cause of the problem - if isinstance(exc, ConfigError) and exc.__cause__ is not None: - exc = exc.__cause__ - - if isinstance(exc, SyntaxError): - path = pathlib.Path(exc.filename or "") - line = (exc.lineno or 1) - 1 - else: - tb = exc.__traceback__ - frame = traceback.extract_tb(tb)[-1] - path = pathlib.Path(frame.filename) - line = (frame.lineno or 1) - 1 - - message = type(exc).__name__ if exc.args.count == 0 else exc.args[0] - - diagnostic = Diagnostic( - range=Range( - start=Position(line=line, character=0), - end=Position(line=line + 1, character=0), - ), - message=message, - severity=DiagnosticSeverity.Error, - ) - - return Uri.from_fs_path(str(path)), diagnostic - - -cli = setup_cli("esbonio.lsp.sphinx", "Esbonio's Sphinx language server.") -cli.set_defaults(modules=DEFAULT_MODULES) -cli.set_defaults(server_cls=SphinxLanguageServer) diff --git a/lib/esbonio/esbonio/lsp/sphinx/__main__.py b/lib/esbonio/esbonio/lsp/sphinx/__main__.py deleted file mode 100644 index ff4219b5e..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys - -from esbonio.cli import main -from esbonio.lsp.sphinx import cli - -sys.exit(main(cli)) diff --git a/lib/esbonio/esbonio/lsp/sphinx/autodoc.py b/lib/esbonio/esbonio/lsp/sphinx/autodoc.py deleted file mode 100644 index b446899b3..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/autodoc.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Iterable -from typing import Optional - -from esbonio.lsp import CompletionContext -from esbonio.lsp.directives import DirectiveLanguageFeature -from esbonio.lsp.directives import Directives -from esbonio.lsp.sphinx import SphinxLanguageServer - - -class AutoDoc(DirectiveLanguageFeature): - def __init__(self, rst: SphinxLanguageServer): - self.rst = rst - - def suggest_options( - self, context: CompletionContext, directive: str, domain: Optional[str] - ) -> Iterable[str]: - if self.rst.app is None or not directive.startswith("auto"): - return [] - - # The autoxxxx set of directives need special support as their options are - # stored on "documenters" instead of the directive implementation itself. - name = directive.replace("auto", "") - documenter = self.rst.app.registry.documenters.get(name, None) - - if documenter is None: - self.rst.logger.debug( - "Unable to find documenter for directive: '%s'", directive - ) - return [] - - return documenter.option_spec.keys() - - -def esbonio_setup(rst: SphinxLanguageServer, directives: Directives): - directives.add_feature(AutoDoc(rst)) diff --git a/lib/esbonio/esbonio/lsp/sphinx/cli.py b/lib/esbonio/esbonio/lsp/sphinx/cli.py deleted file mode 100644 index 1b77b4127..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/cli.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Additional command line utilities for the SphinxLanguageServer.""" -import argparse -import json -import sys -from typing import List - -from esbonio.cli import esbonio_converter -from esbonio.lsp.sphinx import SphinxConfig - - -def config_cmd(args, extra): - if args.to_cli: - config_to_cli(args.to_cli) - return 0 - - return cli_to_config(extra) - - -def config_to_cli(config: str): - converter = esbonio_converter() - conf = converter.structure(json.loads(config), SphinxConfig) - print(" ".join(conf.to_cli_args())) - return 0 - - -def cli_to_config(cli_args: List[str]): - conf = SphinxConfig.from_arguments(cli_args=cli_args) - if conf is None: - return 1 - - converter = esbonio_converter() - print(json.dumps(converter.unstructure(conf), indent=2)) - return 0 - - -cli = argparse.ArgumentParser( - prog="esbonio-sphinx", - description="Supporting commands and utilities for the SphinxLanguageServer.", -) -commands = cli.add_subparsers(title="commands") -config = commands.add_parser( - "config", - usage="%(prog)s [--from-cli] -- ARGS", - description="configuration options helper.", -) -config.set_defaults(run=config_cmd) - -mode = config.add_mutually_exclusive_group() -mode.add_argument( - "--from-cli", - action="store_true", - default=True, - help="convert sphinx-build cli options to esbonio's initialization options.", -) -mode.add_argument( - "--to-cli", - help="convert esbonio's initialization options to sphinx-build options", -) - - -def main(): - try: - idx = sys.argv.index("--") - args, extra = sys.argv[1:idx], sys.argv[idx + 1 :] - except ValueError: - args, extra = sys.argv[1:], None - - parsed_args = cli.parse_args(args) - - if hasattr(parsed_args, "run"): - return parsed_args.run(parsed_args, extra) - - cli.print_help() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/lib/esbonio/esbonio/lsp/sphinx/codeblocks.py b/lib/esbonio/esbonio/lsp/sphinx/codeblocks.py deleted file mode 100644 index bd7eb5746..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/codeblocks.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Support for code-blocks and related directives.""" -import textwrap -from typing import List - -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionItemKind -from lsprotocol.types import MarkupContent -from lsprotocol.types import MarkupKind -from pygments.lexers import get_all_lexers - -from esbonio.lsp.directives import Directives -from esbonio.lsp.rst import CompletionContext -from esbonio.lsp.sphinx import SphinxLanguageServer - - -class CodeBlocks: - def __init__(self, rst: SphinxLanguageServer): - self.rst = rst - self.logger = rst.logger.getChild(self.__class__.__name__) - self._lexers: List[CompletionItem] = [] - - @property - def lexers(self) -> List[CompletionItem]: - if len(self._lexers) == 0: - self._index_lexers() - - return self._lexers - - def complete_arguments( - self, context: CompletionContext, domain: str, name: str - ) -> List[CompletionItem]: - if name in {"code-block", "highlight"}: - return self.lexers - - return [] - - def _index_lexers(self): - self._lexers = [] - - for name, labels, files, mimes in get_all_lexers(): - for label in labels: - documentation = f"""\ - ### {name} - - Filenames: {', '.join(files)} - - MIME Types: {', '.join(mimes)} - """ - - item = CompletionItem( - label=label, - kind=CompletionItemKind.Constant, - documentation=MarkupContent( - kind=MarkupKind.Markdown, value=textwrap.dedent(documentation) - ), - ) - - self._lexers.append(item) - - -def esbonio_setup(rst: SphinxLanguageServer, directives: Directives): - directives.add_argument_completion_provider(CodeBlocks(rst)) diff --git a/lib/esbonio/esbonio/lsp/sphinx/config.py b/lib/esbonio/esbonio/lsp/sphinx/config.py deleted file mode 100644 index b8ae5e7b8..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/config.py +++ /dev/null @@ -1,700 +0,0 @@ -import hashlib -import inspect -import logging -import multiprocessing -import os -import pathlib -import re -import sys -import traceback -from types import ModuleType -from typing import Any -from typing import Dict -from typing import List -from typing import Literal -from typing import Optional -from typing import Tuple -from typing import Union -from unittest import mock - -import attrs -import platformdirs -import pygls.uris as Uri -from lsprotocol.types import Diagnostic -from lsprotocol.types import DiagnosticSeverity -from lsprotocol.types import Position -from lsprotocol.types import Range -from sphinx.application import Sphinx -from sphinx.cmd.build import main as sphinx_build -from sphinx.util.logging import OnceFilter -from sphinx.util.logging import SphinxLogRecord -from sphinx.util.logging import WarningLogRecordTranslator - -from esbonio.lsp.log import LOG_NAMESPACE -from esbonio.lsp.log import LspHandler -from esbonio.lsp.rst.config import ServerConfig - -PATH_VAR_PATTERN = re.compile(r"^\${(\w+)}/?.*") -logger = logging.getLogger(LOG_NAMESPACE) - - -class MissingConfigError(Exception): - """Indicates that we couldn't locate the project's 'conf.py'""" - - -@attrs.define -class SphinxConfig: - """Configuration values to pass to the Sphinx application instance.""" - - build_dir: Optional[str] = attrs.field(default=None) - """The directory to write build outputs into.""" - - builder_name: str = attrs.field(default="html") - """The currently used builder name.""" - - conf_dir: Optional[str] = attrs.field(default=None) - """The directory containing the project's ``conf.py``.""" - - config_overrides: Dict[str, Any] = attrs.field(factory=dict) - """Any overrides to configuration values.""" - - doctree_dir: Optional[str] = attrs.field(default=None) - """The directory to write doctrees into.""" - - force_full_build: bool = attrs.field(default=False) - """Force a full build on startup.""" - - keep_going: bool = attrs.field(default=False) - """Continue building when errors (from warnings) are encountered.""" - - make_mode: bool = attrs.field(default=True) - """Flag indicating if the server should align to "make mode" behavior.""" - - num_jobs: Union[Literal["auto"], int] = attrs.field(default=1) - """The number of jobs to use for parallel builds.""" - - quiet: bool = attrs.field(default=False) - """Hide standard Sphinx output messages""" - - silent: bool = attrs.field(default=False) - """Hide all Sphinx output.""" - - src_dir: Optional[str] = attrs.field(default=None) - """The directory containing the project's source.""" - - tags: List[str] = attrs.field(factory=list) - """Tags to enable during a build.""" - - verbosity: int = attrs.field(default=0) - """The verbosity of Sphinx's output.""" - - version: Optional[str] = attrs.field(default=None) - """Sphinx's version number.""" - - warning_is_error: bool = attrs.field(default=False) - """Treat any warning as an error""" - - @property - def parallel(self) -> int: - """The parsed value of the ``num_jobs`` field.""" - - if self.num_jobs == "auto": - return multiprocessing.cpu_count() - - return self.num_jobs - - @classmethod - def from_arguments( - cls, - *, - cli_args: Optional[List[str]] = None, - sphinx_args: Optional[Dict[str, Any]] = None, - ) -> Optional["SphinxConfig"]: - """Return the ``SphinxConfig`` instance that's equivalent to the given arguments. - - .. note:: - - Only ``cli_args`` **or** ``sphinx_args`` may be given. - - .. warning:: - - This method is unable to determine the value of the - :obj:`SphinxConfig.make_mode` setting when passing ``sphinx_args`` - - - Parameters - ---------- - cli_args - The cli arguments you would normally pass to ``sphinx-build`` - - sphinx_args: - The arguments you would use to create a ``Sphinx`` application instance. - """ - - make_mode: bool = False - neither_given = cli_args is None and sphinx_args is None - both_given = cli_args is not None and sphinx_args is not None - if neither_given or both_given: - raise ValueError("You must pass either 'cli_args' or 'sphinx_args'") - - if cli_args is not None: - # The easiest way to handle this is to just call sphinx-build but with - # the Sphinx app object patched out - then we just use all the args it - # was given! - with mock.patch("sphinx.cmd.build.Sphinx") as m_Sphinx: - sphinx_build(cli_args) - - if m_Sphinx.call_args is None: - return None - - signature = inspect.signature(Sphinx) - keys = signature.parameters.keys() - - values = m_Sphinx.call_args[0] - sphinx_args = {k: v for k, v in zip(keys, values)} - - # `-M` has to be the first argument passed to `sphinx-build` - # https://github.com/sphinx-doc/sphinx/blob/1222bed88eb29cde43a81dd208448dc903c53de2/sphinx/cmd/build.py#L287 - make_mode = cli_args[0] == "-M" - if make_mode and sphinx_args["outdir"].endswith(sphinx_args["buildername"]): - build_dir = pathlib.Path(sphinx_args["outdir"]).parts[:-1] - sphinx_args["outdir"] = str(pathlib.Path(*build_dir)) - - if sphinx_args is None: - return None - - return cls( - conf_dir=sphinx_args.get("confdir", None), - config_overrides=sphinx_args.get("confoverrides", {}), - build_dir=sphinx_args.get("outdir", None), - builder_name=sphinx_args.get("buildername", "html"), - doctree_dir=sphinx_args.get("doctreedir", None), - force_full_build=sphinx_args.get("freshenv", False), - keep_going=sphinx_args.get("keep_going", False), - make_mode=make_mode, - num_jobs=sphinx_args.get("parallel", 1), - quiet=sphinx_args.get("status", 1) is None, - silent=sphinx_args.get("warning", 1) is None, - src_dir=sphinx_args.get("srcdir", None), - tags=sphinx_args.get("tags", []), - verbosity=sphinx_args.get("verbosity", 0), - warning_is_error=sphinx_args.get("warningiserror", False), - ) - - def to_cli_args(self) -> List[str]: - """Convert this into the equivalent ``sphinx-build`` cli arguments.""" - - if self.make_mode: - return self._build_make_cli_args() - - return self._build_cli_args() - - def _build_make_cli_args(self) -> List[str]: - args = ["-M", self.builder_name] - conf_dir = self.conf_dir or "${workspaceRoot}" - src_dir = self.src_dir or conf_dir - - if self.build_dir is None: - build_dir = pathlib.Path(src_dir, "_build") - else: - build_dir = pathlib.Path(self.build_dir) - - args += [src_dir, str(build_dir)] - - args += self._build_standard_args() - default_dtree_dir = str(pathlib.Path(build_dir, "doctrees")) - if self.doctree_dir is not None and self.doctree_dir != default_dtree_dir: - args += ["-d", self.doctree_dir] - - return args - - def _build_cli_args(self) -> List[str]: - args = ["-b", self.builder_name] - - conf_dir = self.conf_dir or "${workspaceRoot}" - src_dir = self.src_dir or conf_dir - - build_dir = self.build_dir or pathlib.Path(src_dir, "_build") - default_dtree_dir = str(pathlib.Path(build_dir, ".doctrees")) - - if self.doctree_dir is not None and self.doctree_dir != default_dtree_dir: - args += ["-d", self.doctree_dir] - - args += self._build_standard_args() - args += [src_dir, str(build_dir)] - return args - - def _build_standard_args(self) -> List[str]: - args: List[str] = [] - - conf_dir = self.conf_dir or "${workspaceRoot}" - src_dir = self.src_dir or self.conf_dir - - if conf_dir != src_dir: - args += ["-c", conf_dir] - - if self.force_full_build: - args += ["-E"] - - if self.parallel > 1: - args += ["-j", str(self.num_jobs)] - - if self.silent: - args += ["-Q"] - - if self.quiet and not self.silent: - args += ["-q"] - - if self.warning_is_error: - args += ["-W"] - - if self.keep_going: - args += ["--keep-going"] - - if self.verbosity > 0: - args += ["-" + ("v" * self.verbosity)] - - for key, value in self.config_overrides.items(): - if key == "nitpicky": - args += ["-n"] - continue - - if key.startswith("html_context."): - char = "A" - key = key.replace("html_context.", "") - else: - char = "D" - - args += [f"-{char}{key}={value}"] - - for tag in self.tags: - args += ["-t", tag] - - return args - - def to_application_args(self) -> Dict[str, Any]: - """Convert this into the equivalent Sphinx application arguments.""" - - return { - "buildername": self.builder_name, - "confdir": self.conf_dir, - "confoverrides": self.config_overrides, - "doctreedir": self.doctree_dir, - "freshenv": self.force_full_build, - "keep_going": self.keep_going, - "outdir": self.build_dir, - "parallel": self.parallel, - "srcdir": self.src_dir, - "status": None, - "tags": self.tags, - "verbosity": self.verbosity, - "warning": None, - "warningiserror": self.warning_is_error, - } - - def resolve(self, root_uri: str) -> "SphinxConfig": - conf_dir = self.resolve_conf_dir(root_uri) - if conf_dir is None: - raise MissingConfigError() - - src_dir = self.resolve_src_dir(root_uri, str(conf_dir)) - build_dir = self.resolve_build_dir(root_uri, str(conf_dir)) - doctree_dir = self.resolve_doctree_dir(root_uri, str(conf_dir), str(build_dir)) - - if self.make_mode: - build_dir /= self.builder_name - - return SphinxConfig( - conf_dir=str(conf_dir), - config_overrides=self.config_overrides, - build_dir=str(build_dir), - builder_name=self.builder_name, - doctree_dir=str(doctree_dir), - force_full_build=self.force_full_build, - keep_going=self.keep_going, - make_mode=self.make_mode, - num_jobs=self.num_jobs, - quiet=self.quiet, - silent=self.silent, - src_dir=str(src_dir), - tags=self.tags, - verbosity=self.verbosity, - warning_is_error=self.warning_is_error, - ) - - def resolve_build_dir(self, root_uri: str, actual_conf_dir: str) -> pathlib.Path: - """Get the build dir to use based on the user's config. - - If nothing is specified in the given ``config``, this will choose a location - within the user's cache dir (as determined by - `platformdirs <https://pypi.org/project/platformdirs>`). The directory name will be a hash - derived from the given ``conf_dir`` for the project. - - Alternatively the user (or least language client) can override this by setting - either an absolute path, or a path based on the following "variables". - - - ``${workspaceRoot}`` which expands to the workspace root as provided - by the language client. - - - ``${workspaceFolder}`` alias for ``${workspaceRoot}``, placeholder ready for - multi-root support. - - - ``${confDir}`` which expands to the configured config dir. - - Parameters - ---------- - root_uri - The workspace root uri - - actual_conf_dir: - The fully resolved conf dir for the project - """ - - if not self.build_dir: - # Try to pick a sensible dir based on the project's location - cache = platformdirs.user_cache_dir("esbonio", "swyddfa") - project = hashlib.md5(str(actual_conf_dir).encode()).hexdigest() - - return pathlib.Path(cache) / project - - root_dir = Uri.to_fs_path(root_uri) - match = PATH_VAR_PATTERN.match(self.build_dir) - - if match and match.group(1) in {"workspaceRoot", "workspaceFolder"}: - build = pathlib.Path(self.build_dir).parts[1:] - return pathlib.Path(root_dir, *build).resolve() - - if match and match.group(1) == "confDir": - build = pathlib.Path(self.build_dir).parts[1:] - return pathlib.Path(actual_conf_dir, *build).resolve() - - # Convert path to/from uri so that any path quirks from windows are - # automatically handled - build_uri = Uri.from_fs_path(self.build_dir) - build_dir = Uri.to_fs_path(build_uri) - - # But make sure paths starting with '~' are not corrupted - if build_dir.startswith("/~"): - build_dir = build_dir.replace("/~", "~") - - # But make sure (windows) paths starting with '~' are not corrupted - if build_dir.startswith("\\~"): - build_dir = build_dir.replace("\\~", "~") - - return pathlib.Path(build_dir).expanduser() - - def resolve_doctree_dir( - self, root_uri: str, actual_conf_dir: str, actual_build_dir: str - ) -> pathlib.Path: - """Get the directory to use for doctrees based on the user's config. - - If ``doctree_dir`` is not set, this method will follow what ``sphinx-build`` - does. - - - If ``make_mode`` is true, this will be set to ``${buildDir}/doctrees`` - - If ``make_mode`` is false, this will be set to ``${buildDir}/.doctrees`` - - Otherwise, if ``doctree_dir`` is set the following "variables" are handled by - this method. - - - ``${workspaceRoot}`` which expands to the workspace root as provided by the - language client. - - - ``${workspaceFolder}`` alias for ``${workspaceRoot}``, placeholder ready for - multi-root support. - - - ``${confDir}`` which expands to the configured config dir. - - - ``${buildDir}`` which expands to the configured build dir. - - Parameters - ---------- - root_uri - The workspace root uri - - actual_conf_dir - The fully resolved conf dir for the project - - actual_build_dir - The fully resolved build dir for the project. - """ - - if self.doctree_dir is None: - if self.make_mode: - return pathlib.Path(actual_build_dir, "doctrees") - - return pathlib.Path(actual_build_dir, ".doctrees") - - root_dir = Uri.to_fs_path(root_uri) - match = PATH_VAR_PATTERN.match(self.doctree_dir) - - if match and match.group(1) in {"workspaceRoot", "workspaceFolder"}: - build = pathlib.Path(self.doctree_dir).parts[1:] - return pathlib.Path(root_dir, *build).resolve() - - if match and match.group(1) == "confDir": - build = pathlib.Path(self.doctree_dir).parts[1:] - return pathlib.Path(actual_conf_dir, *build).resolve() - - if match and match.group(1) == "buildDir": - build = pathlib.Path(self.doctree_dir).parts[1:] - return pathlib.Path(actual_build_dir, *build).resolve() - - return pathlib.Path(self.doctree_dir).expanduser() - - def resolve_conf_dir(self, root_uri: str) -> Optional[pathlib.Path]: - """Get the conf dir to use based on the user's config. - - If ``conf_dir`` is not set, this method will attempt to find it by searching - within the ``root_uri`` for a ``conf.py`` file. If multiple files are found, the - first one found will be chosen. - - If ``conf_dir`` is set the following "variables" are handled by this method - - - ``${workspaceRoot}`` which expands to the workspace root as provided by the - language client. - - - ``${workspaceFolder}`` alias for ``${workspaceRoot}``, placeholder ready for - multi-root support. - - Parameters - ---------- - root_uri - The workspace root uri - """ - root = Uri.to_fs_path(root_uri) - - if not self.conf_dir: - ignore_paths = [".tox", "site-packages"] - - for candidate in pathlib.Path(root).glob("**/conf.py"): - # Skip any files that obviously aren't part of the project - if any(path in str(candidate) for path in ignore_paths): - continue - - return candidate.parent - - # Nothing found - return None - - match = PATH_VAR_PATTERN.match(self.conf_dir) - if not match or match.group(1) not in {"workspaceRoot", "workspaceFolder"}: - return pathlib.Path(self.conf_dir).expanduser() - - conf = pathlib.Path(self.conf_dir).parts[1:] - return pathlib.Path(root, *conf).resolve() - - def resolve_src_dir(self, root_uri: str, actual_conf_dir: str) -> pathlib.Path: - """Get the src dir to use based on the user's config. - - By default the src dir will be the same as the conf dir, but this can - be overriden by setting the ``src_dir`` field. - - There are a number of "variables" that can be included in the path, - currently we support - - - ``${workspaceRoot}`` which expands to the workspace root as provided - by the language client. - - - ``${workspaceFolder}`` alias for ``${workspaceRoot}``, placeholder ready for - multi-root support. - - - ``${confDir}`` which expands to the configured config dir. - - Parameters - ---------- - root_uri - The workspace root uri - - actual_conf_dir - The fully resolved conf dir for the project - """ - - if not self.src_dir: - return pathlib.Path(actual_conf_dir) - - src_dir = self.src_dir - root_dir = Uri.to_fs_path(root_uri) - - match = PATH_VAR_PATTERN.match(src_dir) - if match and match.group(1) in {"workspaceRoot", "workspaceFolder"}: - src = pathlib.Path(src_dir).parts[1:] - return pathlib.Path(root_dir, *src).resolve() - - if match and match.group(1) == "confDir": - src = pathlib.Path(src_dir).parts[1:] - return pathlib.Path(actual_conf_dir, *src).resolve() - - return pathlib.Path(src_dir).expanduser() - - -@attrs.define -class SphinxServerConfig(ServerConfig): - """ - .. deprecated:: 0.12.0 - - This will be removed in v1.0. - Use :confval:`sphinx.quiet (boolean)` and :confval:`sphinx.silent (boolean)` - options instead. - """ - - hide_sphinx_output: bool = attrs.field(default=False) - """A flag to indicate if Sphinx build output should be omitted from the log.""" - - -@attrs.define -class InitializationOptions: - """The initialization options we can expect to receive from a client.""" - - sphinx: SphinxConfig = attrs.field(factory=SphinxConfig) - """The ``esbonio.sphinx.*`` namespace of options.""" - - server: SphinxServerConfig = attrs.field(factory=SphinxServerConfig) - """The ``esbonio.server.*`` namespace of options.""" - - -DIAGNOSTIC_SEVERITY = { - logging.ERROR: DiagnosticSeverity.Error, - logging.INFO: DiagnosticSeverity.Information, - logging.WARNING: DiagnosticSeverity.Warning, -} - - -class SphinxLogHandler(LspHandler): - """A logging handler that can extract errors from Sphinx's build output.""" - - def __init__(self, app, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.app = app - self.translator = WarningLogRecordTranslator(app) - self.only_once = OnceFilter() - self.diagnostics: Dict[str, List[Diagnostic]] = {} - - def get_location(self, location: str) -> Tuple[str, Optional[int]]: - if not location: - conf = pathlib.Path(self.app.confdir, "conf.py") - return (Uri.from_fs_path(str(conf)), None) - - lineno = None - path, parts = self.get_location_path(location) - - if len(parts) == 1: - try: - lineno = int(parts[0]) - except ValueError: - pass - - if len(parts) == 2 and parts[0].startswith("docstring of "): - target = parts[0].replace("docstring of ", "") - lineno = self.get_docstring_location(target, parts[1]) - - return (Uri.from_fs_path(path), lineno) - - def get_location_path(self, location: str) -> Tuple[str, List[str]]: - """Determine the filepath from the given location.""" - - if location.startswith("internal padding before "): - location = location.replace("internal padding before ", "") - - if location.startswith("internal padding after "): - location = location.replace("internal padding after ", "") - - path, *parts = location.split(":") - - # On windows the rest of the path will be the first element of parts - if pathlib.Path(location).drive: - path += f":{parts.pop(0)}" - - # Diagnostics in .. included:: files are reported relative to the process' - # working directory, so ensure the path is absolute. - path = os.path.abspath(path) - - return path, parts - - def get_docstring_location(self, target: str, offset: str) -> Optional[int]: - # The containing module will be the longest substring we can find in target - candidates = [m for m in sys.modules.keys() if target.startswith(m)] + [""] - module = sys.modules.get(sorted(candidates, key=len, reverse=True)[0], None) - - if module is None: - return None - - obj: Union[ModuleType, Any, None] = module - dotted_name = target.replace(module.__name__ + ".", "") - - for name in dotted_name.split("."): - obj = getattr(obj, name, None) - if obj is None: - return None - - try: - _, line = inspect.getsourcelines(obj) # type: ignore - - # Correct off by one error for docstrings that don't start with a newline. - nl = (obj.__doc__ or "").startswith("\n") - return line + int(offset) - (not nl) - except Exception: - logger.debug( - "Unable to determine diagnostic location\n%s", traceback.format_exc() - ) - return None - - def emit(self, record: logging.LogRecord) -> None: - conditions = [ - "sphinx" not in record.name, - record.levelno not in {logging.WARNING, logging.ERROR}, - not self.translator, - ] - - if any(conditions): - # Log the record as normal - super().emit(record) - return - - # Only process errors/warnings once. - if not self.only_once.filter(record): - return - - # Let sphinx do what it does to warning/error messages - self.translator.filter(record) # type: ignore - - loc = record.location if isinstance(record, SphinxLogRecord) else "" - doc, lineno = self.get_location(loc) - line = lineno or 1 - logger.debug("Reporting diagnostic at %s:%s", doc, line) - - try: - # Not every message contains a string... - if not isinstance(record.msg, str): - message = str(record.msg) - else: - message = record.msg - - # Only attempt to format args if there are args to format - if record.args is not None and len(record.args) > 0: - message = message % record.args - - except Exception: - message = str(record.msg) - logger.error( - "Unable to format diagnostic message: %s", traceback.format_exc() - ) - - diagnostic = Diagnostic( - range=Range( - start=Position(line=line - 1, character=0), - end=Position(line=line, character=0), - ), - message=message, - severity=DIAGNOSTIC_SEVERITY.get( - record.levelno, DiagnosticSeverity.Warning - ), - ) - - if doc not in self.diagnostics: - self.diagnostics[doc] = [diagnostic] - else: - self.diagnostics[doc].append(diagnostic) - - super().emit(record) diff --git a/lib/esbonio/esbonio/lsp/sphinx/directives.json b/lib/esbonio/esbonio/lsp/sphinx/directives.json deleted file mode 100644 index 2ff971a57..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/directives.json +++ /dev/null @@ -1,2627 +0,0 @@ -{ - "c:alias(sphinx.domains.c.CAliasObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-alias", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:alias", - "is_markdown": true, - "description": [ - "Insert one or more alias declarations. Each entity can be specified", - "as they can in the [c:any](https://www.sphinx-doc.org/en/master/_sources/usage/restructuredtext/domains.rst.txt#None) role.", - "", - "For example:", - "", - "```", - ".. c:var:: int data", - ".. c:function:: int f(double k)", - "", - ".. c:alias:: data", - " f", - "```", - "", - "becomes", - "", - "", - "", - "", - "", - "", - "", - "", - "Options", - "", - "Insert nested declarations as well, up to the total depth given.", - "Use 0 for infinite depth and 1 for just the mentioned declaration.", - "Defaults to 1.", - "", - "", - "", - "", - "", - "Skip the mentioned declarations and only render nested declarations.", - "Requires `maxdepth` either 0 or at least 2." - ], - "options": {} - }, - "c:enum(sphinx.domains.c.CEnumObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-enum", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:enum", - "is_markdown": true, - "description": [ - "Describes a C enum." - ], - "options": {} - }, - "c:enumerator(sphinx.domains.c.CEnumeratorObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-enumerator", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:enumerator", - "is_markdown": true, - "description": [ - "Describes a C enumerator." - ], - "options": {} - }, - "c:function(sphinx.domains.c.CFunctionObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-function", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:function", - "is_markdown": true, - "description": [ - "Describes a C function. The signature should be given as in C, e.g.:", - "", - "```", - ".. c:function:: PyObject *PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)", - "```", - "", - "Note that you don't have to backslash-escape asterisks in the signature, as", - "it is not parsed by the reST inliner.", - "", - "In the description of a function you can use the following info fields", - "(see also [info-field-lists](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists)).", - "- `param`, `parameter`, `arg`, `argument`,", - "Description of a parameter.", - "- `type`: Type of a parameter,", - "written as if passed to the [c:expr](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-expr) role.", - "- `returns`, `return`: Description of the return value.", - "- `rtype`: Return type,", - "written as if passed to the [c:expr](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-expr) role.", - "- `retval`, `retvals`: An alternative to `returns` for describing", - "the result of the function.", - "", - "", - "", - "For example:", - "", - "```", - ".. c:function:: PyObject *PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)", - "", - " :param type: description of the first parameter.", - " :param nitems: description of the second parameter.", - " :returns: a result.", - " :retval NULL: under some conditions.", - " :retval NULL: under some other conditions as well.", - "```", - "", - "which renders as", - "", - "** for some editors (e.g., vim) to stop bold-highlighting the source", - "| | |", - "|-|-|", - "| param type | description of the first parameter. |", - "| param nitems | description of the second parameter. |", - "| returns | a result. |", - "| retval NULL | under some conditions. |", - "| retval NULL | under some other conditions as well. |" - ], - "options": {} - }, - "c:macro(sphinx.domains.c.CMacroObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-macro", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:macro", - "is_markdown": true, - "description": [ - "Describes a C macro, i.e., a C-language `#define`, without the replacement", - "text.", - "", - "In the description of a macro you can use the same info fields as for the", - "[c:function](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-function) directive." - ], - "options": {} - }, - "c:member(sphinx.domains.c.CMemberObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-member", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:member", - "is_markdown": true, - "description": [ - "Describes a C struct member or variable. Example signature:", - "", - "```", - ".. c:member:: PyObject *PyTypeObject.tp_bases", - "```", - "", - "The difference between the two directives is only cosmetic." - ], - "options": {} - }, - "c:namespace(sphinx.domains.c.CNamespaceObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-namespace", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:namespace", - "is_markdown": true, - "description": [ - "Changes the current scope for the subsequent objects to the given scope, and", - "resets the namespace directive stack. Note that nested scopes can be", - "specified by separating with a dot, e.g.:", - "", - "```", - ".. c:namespace:: Namespace1.Namespace2.SomeStruct.AnInnerStruct", - "```", - "", - "All subsequent objects will be defined as if their name were declared with", - "the scope prepended. The subsequent cross-references will be searched for", - "starting in the current scope.", - "", - "Using `NULL` or `0` as the scope will change to global scope." - ], - "options": {} - }, - "c:namespace-pop(sphinx.domains.c.CNamespacePopObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-namespace-pop", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:namespace-pop", - "is_markdown": true, - "description": [ - "Undo the previous `c:namespace-push` directive (not just pop a scope).", - "For example, after:", - "", - "```", - ".. c:namespace:: A.B", - "", - ".. c:namespace-push:: C.D", - "", - ".. c:namespace-pop::", - "```", - "", - "the current scope will be `A.B` (not `A.B.C`).", - "", - "If no previous `c:namespace-push` directive has been used, but only a", - "`c:namespace` directive, then the current scope will be reset to global", - "scope. That is, `.. c:namespace:: A.B` is equivalent to:", - "", - "```", - ".. c:namespace:: NULL", - "", - ".. c:namespace-push:: A.B", - "```" - ], - "options": {} - }, - "c:namespace-push(sphinx.domains.c.CNamespacePushObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-namespace-push", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:namespace-push", - "is_markdown": true, - "description": [ - "Change the scope relatively to the current scope. For example, after:", - "", - "```", - ".. c:namespace:: A.B", - "", - ".. c:namespace-push:: C.D", - "```", - "", - "the current scope will be `A.B.C.D`." - ], - "options": {} - }, - "c:struct(sphinx.domains.c.CStructObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-struct", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:struct", - "is_markdown": true, - "description": [ - "Describes a C struct." - ], - "options": {} - }, - "c:type(sphinx.domains.c.CTypeObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-type", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:type", - "is_markdown": true, - "description": [ - "Describes a C type, either as a typedef, or the alias for an unspecified", - "type." - ], - "options": {} - }, - "c:union(sphinx.domains.c.CUnionObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-union", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:union", - "is_markdown": true, - "description": [ - "Describes a C union." - ], - "options": {} - }, - "c:var(sphinx.domains.c.CMemberObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-c-var", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:var", - "is_markdown": true, - "description": [ - "Describes a C struct member or variable. Example signature:", - "", - "```", - ".. c:member:: PyObject *PyTypeObject.tp_bases", - "```", - "", - "The difference between the two directives is only cosmetic." - ], - "options": {} - }, - "cpp:alias(sphinx.domains.cpp.CPPAliasObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-alias", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:alias", - "is_markdown": true, - "description": [ - "Insert one or more alias declarations. Each entity can be specified", - "as they can in the [cpp:any](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-any) role.", - "If the name of a function is given (as opposed to the complete signature),", - "then all overloads of the function will be listed.", - "", - "For example:", - "", - "```", - ".. cpp:alias:: Data::a", - " overload_example::C::f", - "```", - "", - "becomes", - "", - "", - "", - "whereas:", - "", - "```", - ".. cpp:alias:: void overload_example::C::f(double d) const", - " void overload_example::C::f(double d)", - "```", - "", - "becomes", - "", - "", - "", - "", - "Options", - "", - "Insert nested declarations as well, up to the total depth given.", - "Use 0 for infinite depth and 1 for just the mentioned declaration.", - "Defaults to 1.", - "", - "", - "", - "", - "", - "Skip the mentioned declarations and only render nested declarations.", - "Requires `maxdepth` either 0 or at least 2." - ], - "options": {} - }, - "cpp:class(sphinx.domains.cpp.CPPClassObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-class", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:class", - "is_markdown": true, - "description": [ - "Describe a class/struct, possibly with specification of inheritance, e.g.,:", - "", - "```", - ".. cpp:class:: MyClass : public MyBase, MyOtherBase", - "```", - "", - "The difference between [cpp:class](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-class) and [cpp:struct](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-struct) is", - "only cosmetic: the prefix rendered in the output, and the specifier shown", - "in the index.", - "", - "The class can be directly declared inside a nested scope, e.g.,:", - "", - "```", - ".. cpp:class:: OuterScope::MyClass : public MyBase, MyOtherBase", - "```", - "", - "A class template can be declared:", - "", - "```", - ".. cpp:class:: template<typename T, std::size_t N> std::array", - "```", - "", - "or with a line break:", - "", - "```", - ".. cpp:class:: template<typename T, std::size_t N> \\", - " std::array", - "```", - "", - "Full and partial template specialisations can be declared:", - "", - "```", - ".. cpp:class:: template<> \\", - " std::array<bool, 256>", - "", - ".. cpp:class:: template<typename T> \\", - " std::array<T, 42>", - "```" - ], - "options": {} - }, - "cpp:concept(sphinx.domains.cpp.CPPConceptObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-concept", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:concept", - "is_markdown": true, - "description": [ - "The support for concepts is experimental. It is based on the", - "current draft standard and the Concepts Technical Specification.", - "The features may change as they evolve.", - "", - "Describe a concept. It must have exactly 1 template parameter list. The name", - "may be a nested name. Example:", - "", - "```", - ".. cpp:concept:: template<typename It> std::Iterator", - "", - " Proxy to an element of a notional sequence that can be compared,", - " indirected, or incremented.", - "", - " **Notation**", - "", - " .. cpp:var:: It r", - "", - " An lvalue.", - "", - " **Valid Expressions**", - "", - " - :cpp:expr:`*r`, when :cpp:expr:`r` is dereferenceable.", - " - :cpp:expr:`++r`, with return type :cpp:expr:`It&`, when", - " :cpp:expr:`r` is incrementable.", - "```", - "", - "This will render as follows:", - "", - "", - "Proxy to an element of a notional sequence that can be compared,", - "indirected, or incremented.", - "", - "Notation", - "", - "", - "An lvalue.", - "", - "", - "Valid Expressions", - "- `*r`, when `r` is dereferenceable.", - "- `++r`, with return type `It&`, when `r`", - "is incrementable." - ], - "options": {} - }, - "cpp:enum(sphinx.domains.cpp.CPPEnumObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-enum", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:enum", - "is_markdown": true, - "description": [ - "Describe a (scoped) enum, possibly with the underlying type specified. Any", - "enumerators declared inside an unscoped enum will be declared both in the", - "enum scope and in the parent scope. Examples:", - "", - "```", - ".. cpp:enum:: MyEnum", - "", - " An unscoped enum.", - "", - ".. cpp:enum:: MySpecificEnum : long", - "", - " An unscoped enum with specified underlying type.", - "", - ".. cpp:enum-class:: MyScopedEnum", - "", - " A scoped enum.", - "", - ".. cpp:enum-struct:: protected MyScopedVisibilityEnum : std::underlying_type<MySpecificEnum>::type", - "", - " A scoped enum with non-default visibility, and with a specified", - " underlying type.", - "```" - ], - "options": {} - }, - "cpp:enum-class(sphinx.domains.cpp.CPPEnumObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-enum-class", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:enum-class", - "is_markdown": true, - "description": [ - "Describe a (scoped) enum, possibly with the underlying type specified. Any", - "enumerators declared inside an unscoped enum will be declared both in the", - "enum scope and in the parent scope. Examples:", - "", - "```", - ".. cpp:enum:: MyEnum", - "", - " An unscoped enum.", - "", - ".. cpp:enum:: MySpecificEnum : long", - "", - " An unscoped enum with specified underlying type.", - "", - ".. cpp:enum-class:: MyScopedEnum", - "", - " A scoped enum.", - "", - ".. cpp:enum-struct:: protected MyScopedVisibilityEnum : std::underlying_type<MySpecificEnum>::type", - "", - " A scoped enum with non-default visibility, and with a specified", - " underlying type.", - "```" - ], - "options": {} - }, - "cpp:enum-struct(sphinx.domains.cpp.CPPEnumObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-enum-struct", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:enum-struct", - "is_markdown": true, - "description": [ - "Describe a (scoped) enum, possibly with the underlying type specified. Any", - "enumerators declared inside an unscoped enum will be declared both in the", - "enum scope and in the parent scope. Examples:", - "", - "```", - ".. cpp:enum:: MyEnum", - "", - " An unscoped enum.", - "", - ".. cpp:enum:: MySpecificEnum : long", - "", - " An unscoped enum with specified underlying type.", - "", - ".. cpp:enum-class:: MyScopedEnum", - "", - " A scoped enum.", - "", - ".. cpp:enum-struct:: protected MyScopedVisibilityEnum : std::underlying_type<MySpecificEnum>::type", - "", - " A scoped enum with non-default visibility, and with a specified", - " underlying type.", - "```" - ], - "options": {} - }, - "cpp:enumerator(sphinx.domains.cpp.CPPEnumeratorObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-enumerator", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:enumerator", - "is_markdown": true, - "description": [ - "Describe an enumerator, optionally with its value defined, e.g.,:", - "", - "```", - ".. cpp:enumerator:: MyEnum::myEnumerator", - "", - ".. cpp:enumerator:: MyEnum::myOtherEnumerator = 42", - "```" - ], - "options": {} - }, - "cpp:function(sphinx.domains.cpp.CPPFunctionObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-function", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:function", - "is_markdown": true, - "description": [ - "Describe a function or member function, e.g.,:", - "", - "```", - ".. cpp:function:: bool myMethod(int arg1, std::string arg2)", - "", - " A function with parameters and types.", - "", - ".. cpp:function:: bool myMethod(int, double)", - "", - " A function with unnamed parameters.", - "", - ".. cpp:function:: const T &MyClass::operator[](std::size_t i) const", - "", - " An overload for the indexing operator.", - "", - ".. cpp:function:: operator bool() const", - "", - " A casting operator.", - "", - ".. cpp:function:: constexpr void foo(std::string &bar[2]) noexcept", - "", - " A constexpr function.", - "", - ".. cpp:function:: MyClass::MyClass(const MyClass&) = default", - "", - " A copy constructor with default implementation.", - "```", - "", - "Function templates can also be described:", - "", - "```", - ".. cpp:function:: template<typename U> \\", - " void print(U &&u)", - "```", - "", - "and function template specialisations:", - "", - "```", - ".. cpp:function:: template<> \\", - " void print(int i)", - "```" - ], - "options": {} - }, - "cpp:member(sphinx.domains.cpp.CPPMemberObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-member", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:member", - "is_markdown": true, - "description": [ - "Describe a variable or member variable, e.g.,:", - "", - "```", - ".. cpp:member:: std::string MyClass::myMember", - "", - ".. cpp:var:: std::string MyClass::myOtherMember[N][M]", - "", - ".. cpp:member:: int a = 42", - "```", - "", - "Variable templates can also be described:", - "", - "```", - ".. cpp:member:: template<class T> \\", - " constexpr T pi = T(3.1415926535897932385)", - "```" - ], - "options": {} - }, - "cpp:namespace(sphinx.domains.cpp.CPPNamespaceObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-namespace", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:namespace", - "is_markdown": true, - "description": [ - "Changes the current scope for the subsequent objects to the given scope, and", - "resets the namespace directive stack. Note that the namespace does not need", - "to correspond to C++ namespaces, but can end in names of classes, e.g.,:", - "", - "```", - ".. cpp:namespace:: Namespace1::Namespace2::SomeClass::AnInnerClass", - "```", - "", - "All subsequent objects will be defined as if their name were declared with", - "the scope prepended. The subsequent cross-references will be searched for", - "starting in the current scope.", - "", - "Using `NULL`, `0`, or `nullptr` as the scope will change to global", - "scope.", - "", - "A namespace declaration can also be templated, e.g.,:", - "", - "```", - ".. cpp:class:: template<typename T> \\", - " std::vector", - "", - ".. cpp:namespace:: template<typename T> std::vector", - "", - ".. cpp:function:: std::size_t size() const", - "```", - "", - "declares `size` as a member function of the class template", - "`std::vector`. Equivalently this could have been declared using:", - "", - "```", - ".. cpp:class:: template<typename T> \\", - " std::vector", - "", - " .. cpp:function:: std::size_t size() const", - "```", - "", - "or:", - "", - "```", - ".. cpp:class:: template<typename T> \\", - " std::vector", - "```" - ], - "options": {} - }, - "cpp:namespace-pop(sphinx.domains.cpp.CPPNamespacePopObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-namespace-pop", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:namespace-pop", - "is_markdown": true, - "description": [ - "Undo the previous `cpp:namespace-push` directive (not just pop a scope).", - "For example, after:", - "", - "```", - ".. cpp:namespace:: A::B", - "", - ".. cpp:namespace-push:: C::D", - "", - ".. cpp:namespace-pop::", - "```", - "", - "the current scope will be `A::B` (not `A::B::C`).", - "", - "If no previous `cpp:namespace-push` directive has been used, but only a", - "`cpp:namespace` directive, then the current scope will be reset to global", - "scope. That is, `.. cpp:namespace:: A::B` is equivalent to:", - "", - "```", - ".. cpp:namespace:: nullptr", - "", - ".. cpp:namespace-push:: A::B", - "```" - ], - "options": {} - }, - "cpp:namespace-push(sphinx.domains.cpp.CPPNamespacePushObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-namespace-push", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:namespace-push", - "is_markdown": true, - "description": [ - "Change the scope relatively to the current scope. For example, after:", - "", - "```", - ".. cpp:namespace:: A::B", - "", - ".. cpp:namespace-push:: C::D", - "```", - "", - "the current scope will be `A::B::C::D`." - ], - "options": {} - }, - "cpp:struct(sphinx.domains.cpp.CPPClassObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-struct", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:struct", - "is_markdown": true, - "description": [ - "Describe a class/struct, possibly with specification of inheritance, e.g.,:", - "", - "```", - ".. cpp:class:: MyClass : public MyBase, MyOtherBase", - "```", - "", - "The difference between [cpp:class](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-class) and [cpp:struct](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-struct) is", - "only cosmetic: the prefix rendered in the output, and the specifier shown", - "in the index.", - "", - "The class can be directly declared inside a nested scope, e.g.,:", - "", - "```", - ".. cpp:class:: OuterScope::MyClass : public MyBase, MyOtherBase", - "```", - "", - "A class template can be declared:", - "", - "```", - ".. cpp:class:: template<typename T, std::size_t N> std::array", - "```", - "", - "or with a line break:", - "", - "```", - ".. cpp:class:: template<typename T, std::size_t N> \\", - " std::array", - "```", - "", - "Full and partial template specialisations can be declared:", - "", - "```", - ".. cpp:class:: template<> \\", - " std::array<bool, 256>", - "", - ".. cpp:class:: template<typename T> \\", - " std::array<T, 42>", - "```" - ], - "options": {} - }, - "cpp:type(sphinx.domains.cpp.CPPTypeObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-type", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:type", - "is_markdown": true, - "description": [ - "Describe a type as in a typedef declaration, a type alias declaration, or", - "simply the name of a type with unspecified type, e.g.,:", - "", - "```", - ".. cpp:type:: std::vector<int> MyList", - "", - " A typedef-like declaration of a type.", - "", - ".. cpp:type:: MyContainer::const_iterator", - "", - " Declaration of a type alias with unspecified type.", - "", - ".. cpp:type:: MyType = std::unordered_map<int, std::string>", - "", - " Declaration of a type alias.", - "```", - "", - "A type alias can also be templated:", - "", - "```", - ".. cpp:type:: template<typename T> \\", - " MyContainer = std::vector<T>", - "```", - "", - "The example are rendered as follows.", - "", - "", - "A typedef-like declaration of a type.", - "", - "", - "", - "Declaration of a type alias with unspecified type.", - "", - "", - "", - "Declaration of a type alias." - ], - "options": {} - }, - "cpp:union(sphinx.domains.cpp.CPPUnionObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-union", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:union", - "is_markdown": true, - "description": [ - "Describe a union." - ], - "options": {} - }, - "cpp:var(sphinx.domains.cpp.CPPMemberObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-cpp-var", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:var", - "is_markdown": true, - "description": [ - "Describe a variable or member variable, e.g.,:", - "", - "```", - ".. cpp:member:: std::string MyClass::myMember", - "", - ".. cpp:var:: std::string MyClass::myOtherMember[N][M]", - "", - ".. cpp:member:: int a = 42", - "```", - "", - "Variable templates can also be described:", - "", - "```", - ".. cpp:member:: template<class T> \\", - " constexpr T pi = T(3.1415926535897932385)", - "```" - ], - "options": {} - }, - "default-domain(sphinx.directives.DefaultDomain)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-default-domain", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "default-domain", - "is_markdown": true, - "description": [ - "Select a new default domain. While the [primary_domain](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-primary_domain) selects a", - "global default, this only has an effect within the same file." - ], - "options": {} - }, - "describe(sphinx.directives.ObjectDescription)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-describe", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "describe", - "is_markdown": true, - "description": [ - "This directive produces the same formatting as the specific ones provided by", - "domains, but does not create index entries or cross-referencing targets.", - "Example:", - "", - "```", - ".. describe:: PAPER", - "", - " You can set this variable to select a paper size.", - "```" - ], - "options": {} - }, - "envvar(sphinx.domains.std.EnvVar)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-envvar", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "envvar", - "is_markdown": true, - "description": [ - "Describes an environment variable that the documented code or program uses", - "or defines. Referenceable by [envvar](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-envvar)." - ], - "options": {} - }, - "js:attribute(sphinx.domains.javascript.JSObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-js-attribute", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:attribute", - "is_markdown": true, - "description": [ - "Describes the attribute name of object." - ], - "options": {} - }, - "js:class(sphinx.domains.javascript.JSConstructor)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-js-class", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:class", - "is_markdown": true, - "description": [ - "Describes a constructor that creates an object. This is basically like a", - "function but will show up with a class prefix:", - "", - "```", - ".. js:class:: MyAnimal(name[, age])", - "", - " :param string name: The name of the animal", - " :param number age: an optional age for the animal", - "```", - "", - "This is rendered as:", - "", - "", - "| | |", - "|-|-|", - "| param string name | The name of the animal |", - "| param number age | an optional age for the animal |" - ], - "options": {} - }, - "js:data(sphinx.domains.javascript.JSObject)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-js-data", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:data", - "is_markdown": true, - "description": [ - "Describes a global variable or constant." - ], - "options": {} - }, - "js:function(sphinx.domains.javascript.JSCallable)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-js-function", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:function", - "is_markdown": true, - "description": [ - "Describes a JavaScript function or method. If you want to describe", - "arguments as optional use square brackets as [documented ](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#signatures)", - "for Python signatures.", - "", - "You can use fields to give more details about arguments and their expected", - "types, errors which may be thrown by the function, and the value being", - "returned:", - "", - "```", - ".. js:function:: $.getJSON(href, callback[, errback])", - "", - " :param string href: An URI to the location of the resource.", - " :param callback: Gets called with the object.", - " :param errback:", - " Gets called in case the request fails. And a lot of other", - " text so we need multiple lines.", - " :throws SomeError: For whatever reason in that case.", - " :returns: Something.", - "```", - "", - "This is rendered as:", - "", - "", - "| | |", - "|-|-|", - "| param string href | An URI to the location of the resource. |", - "| param callback | Gets called with the object. |", - "| param errback | Gets called in case the request fails. And a lot of other", - "text so we need multiple lines. |", - "| throws SomeError | For whatever reason in that case. |", - "| returns | Something. |" - ], - "options": {} - }, - "js:method(sphinx.domains.javascript.JSCallable)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-js-method", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:method", - "is_markdown": true, - "description": [ - "This directive is an alias for [js:function](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-js-function), however it describes", - "a function that is implemented as a method on a class object." - ], - "options": {} - }, - "js:module(sphinx.domains.javascript.JSModule)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-js-module", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:module", - "is_markdown": true, - "description": [ - "This directive sets the module name for object declarations that follow", - "after. The module name is used in the global module index and in cross", - "references. This directive does not create an object heading like", - "[py:class](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-class) would, for example.", - "", - "By default, this directive will create a linkable entity and will cause an", - "entry in the global module index, unless the `noindex` option is", - "specified. If this option is specified, the directive will only update the", - "current module name." - ], - "options": {} - }, - "object(sphinx.directives.ObjectDescription)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-object", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "object", - "is_markdown": true, - "description": [ - "This directive produces the same formatting as the specific ones provided by", - "domains, but does not create index entries or cross-referencing targets.", - "Example:", - "", - "```", - ".. describe:: PAPER", - "", - " You can set this variable to select a paper size.", - "```" - ], - "options": {} - }, - "option(sphinx.domains.std.Cmdoption)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-option", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "option", - "is_markdown": true, - "description": [ - "Describes a command line argument or switch. Option argument names should", - "be enclosed in angle brackets. Examples:", - "", - "```", - ".. option:: dest_dir", - "", - " Destination directory.", - "", - ".. option:: -m <module>, --module <module>", - "", - " Run a module as a script.", - "```", - "", - "The directive will create cross-reference targets for the given options,", - "referenceable by [option](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-option) (in the example case, you'd use something", - "like `:option:`dest_dir``, `:option:`-m``, or `:option:`--module``).", - "", - "`cmdoption` directive is a deprecated alias for the `option` directive." - ], - "options": {} - }, - "program(sphinx.domains.std.Program)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-program", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "program", - "is_markdown": true, - "description": [ - "Like [py:currentmodule](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-currentmodule), this directive produces no output.", - "Instead, it serves to notify Sphinx that all following [option](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-option)", - "directives document options for the program called name.", - "", - "If you use [program](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-program), you have to qualify the references in your", - "[option](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-option) roles by the program name, so if you have the following", - "situation", - "", - "```", - ".. program:: rm", - "", - ".. option:: -r", - "", - " Work recursively.", - "", - ".. program:: svn", - "", - ".. option:: -r <revision>", - "", - " Specify the revision to work upon.", - "```", - "", - "then `:option:`rm -r`` would refer to the first option, while", - "`:option:`svn -r`` would refer to the second one.", - "", - "If `None` is passed to the argument, the directive will reset the", - "current program name.", - "", - "The program name may contain spaces (in case you want to document", - "subcommands like `svn add` and `svn commit` separately)." - ], - "options": {} - }, - "py:attribute(sphinx.domains.python.PyAttribute)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-attribute", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:attribute", - "is_markdown": true, - "description": [ - "Describes an object data attribute. The description should include", - "information about the type of the data to be expected and whether it may be", - "changed directly.", - "options", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "Describe the location where the object is defined if the object is", - "imported from other modules" - ], - "options": {} - }, - "py:class(sphinx.domains.python.PyClasslike)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-class", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:class", - "is_markdown": true, - "description": [ - "Describes a class. The signature can optionally include parentheses with", - "parameters which will be shown as the constructor arguments. See also", - "[signatures](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#signatures).", - "", - "Methods and attributes belonging to the class should be placed in this", - "directive's body. If they are placed outside, the supplied name should", - "contain the class name so that cross-references still work. Example:", - "", - "```", - ".. py:class:: Foo", - "", - " .. py:method:: quux()", - "", - "-- or --", - "", - ".. py:class:: Bar", - "", - ".. py:method:: Bar.quux()", - "```", - "", - "The first way is the preferred one.", - "options", - "", - "Describe the location where the object is defined if the object is", - "imported from other modules", - "", - "", - "", - "", - "", - "Indicate the class is a final class." - ], - "options": {} - }, - "py:classmethod(sphinx.domains.python.PyClassMethod)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-classmethod", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:classmethod", - "is_markdown": true, - "description": [ - "Like [py:method](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-method), but indicates that the method is a class method." - ], - "options": {} - }, - "py:currentmodule(sphinx.domains.python.PyCurrentModule)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-currentmodule", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:currentmodule", - "is_markdown": true, - "description": [ - "This directive tells Sphinx that the classes, functions etc. documented from", - "here are in the given module (like [py:module](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-module)), but it will not", - "create index entries, an entry in the Global Module Index, or a link target", - "for [py:mod](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-mod). This is helpful in situations where documentation", - "for things in a module is spread over multiple files or sections -- one", - "location has the [py:module](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-module) directive, the others only", - "[py:currentmodule](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-currentmodule)." - ], - "options": {} - }, - "py:data(sphinx.domains.python.PyVariable)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-data", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:data", - "is_markdown": true, - "description": [ - "Describes global data in a module, including both variables and values used", - "as \"defined constants.\" Class and object attributes are not documented", - "using this environment.", - "options", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "Describe the location where the object is defined if the object is", - "imported from other modules" - ], - "options": {} - }, - "py:decorator(sphinx.domains.python.PyDecoratorFunction)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-decorator", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:decorator", - "is_markdown": true, - "description": [ - "Describes a decorator function. The signature should represent the usage as", - "a decorator. For example, given the functions", - "", - "```", - "def removename(func):", - " func.__name__ = ''", - " return func", - "", - "def setnewname(name):", - " def decorator(func):", - " func.__name__ = name", - " return func", - " return decorator", - "```", - "", - "the descriptions should look like this:", - "", - "```", - ".. py:decorator:: removename", - "", - " Remove name of the decorated function.", - "", - ".. py:decorator:: setnewname(name)", - "", - " Set name of the decorated function to *name*.", - "```", - "", - "(as opposed to `.. py:decorator:: removename(func)`.)", - "", - "There is no `py:deco` role to link to a decorator that is marked up with", - "this directive; rather, use the [py:func](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-func) role." - ], - "options": {} - }, - "py:decoratormethod(sphinx.domains.python.PyDecoratorMethod)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-decoratormethod", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:decoratormethod", - "is_markdown": true, - "description": [ - "Same as [py:decorator](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-decorator), but for decorators that are methods.", - "", - "Refer to a decorator method using the [py:meth](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-meth) role." - ], - "options": {} - }, - "py:exception(sphinx.domains.python.PyClasslike)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-exception", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:exception", - "is_markdown": true, - "description": [ - "Describes an exception class. The signature can, but need not include", - "parentheses with constructor arguments.", - "options", - "", - "Indicate the class is a final class." - ], - "options": {} - }, - "py:function(sphinx.domains.python.PyFunction)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-function", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:function", - "is_markdown": true, - "description": [ - "Describes a module-level function. The signature should include the", - "parameters as given in the Python function definition, see [signatures](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#signatures).", - "For example:", - "", - "```", - ".. py:function:: Timer.repeat(repeat=3, number=1000000)", - "```", - "", - "For methods you should use [py:method](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-method).", - "", - "The description normally includes information about the parameters required", - "and how they are used (especially whether mutable objects passed as", - "parameters are modified), side effects, and possible exceptions.", - "", - "This information can (in any `py` directive) optionally be given in a", - "structured form, see [info-field-lists](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists).", - "options", - "", - "Indicate the function is an async function.", - "", - "", - "", - "", - "", - "Describe the location where the object is defined if the object is", - "imported from other modules" - ], - "options": {} - }, - "py:method(sphinx.domains.python.PyMethod)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-method", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:method", - "is_markdown": true, - "description": [ - "Describes an object method. The parameters should not include the `self`", - "parameter. The description should include similar information to that", - "described for `function`. See also [signatures](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#signatures) and", - "[info-field-lists](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists).", - "options", - "", - "Indicate the method is an abstract method.", - "", - "", - "", - "", - "", - "Indicate the method is an async method.", - "", - "", - "", - "", - "", - "Describe the location where the object is defined if the object is", - "imported from other modules", - "", - "", - "", - "", - "", - "Indicate the method is a class method.", - "", - "", - "", - "", - "", - "Indicate the class is a final method.", - "", - "", - "", - "", - "", - "Indicate the method is a property.", - "", - "", - "", - "", - "Use [py:property](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-property) instead.", - "", - "", - "", - "", - "Indicate the method is a static method." - ], - "options": {} - }, - "py:module(sphinx.domains.python.PyModule)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-module", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:module", - "is_markdown": true, - "description": [ - "This directive marks the beginning of the description of a module (or package", - "submodule, in which case the name should be fully qualified, including the", - "package name). It does not create content (like e.g. [py:class](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-class)", - "does).", - "", - "This directive will also cause an entry in the global module index.", - "options", - "", - "Indicate platforms which the module is available (if it is available on", - "all platforms, the option should be omitted). The keys are short", - "identifiers; examples that are in use include \"IRIX\", \"Mac\", \"Windows\"", - "and \"Unix\". It is important to use a key which has already been used when", - "applicable.", - "", - "", - "", - "Consist of one sentence describing the module's purpose -- it is currently", - "only used in the Global Module Index.", - "", - "", - "", - "Mark a module as deprecated; it will be designated as such in various", - "locations then." - ], - "options": {} - }, - "py:property(sphinx.domains.python.PyProperty)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-property", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:property", - "is_markdown": true, - "description": [ - "Describes an object property.", - "", - "", - "options", - "", - "Indicate the property is abstract.", - "", - "", - "", - "Indicate the property is a classmethod.", - "versionaddedd: 4.2" - ], - "options": {} - }, - "py:staticmethod(sphinx.domains.python.PyStaticMethod)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-staticmethod", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:staticmethod", - "is_markdown": true, - "description": [ - "Like [py:method](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-py-method), but indicates that the method is a static method." - ], - "options": {} - }, - "rst:directive(sphinx.domains.rst.ReSTDirective)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-rst-directive", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "rst:directive", - "is_markdown": true, - "description": [ - "Describes a reST directive. The name can be a single directive name or", - "actual directive syntax (.. prefix and :: suffix) with arguments that", - "will be rendered differently. For example:", - "", - "```", - ".. rst:directive:: foo", - "", - " Foo description.", - "", - ".. rst:directive:: .. bar:: baz", - "", - " Bar description.", - "```", - "", - "will be rendered as:", - "", - "", - "Foo description.", - "", - "", - "", - "Bar description." - ], - "options": {} - }, - "rst:directive:option(sphinx.domains.rst.ReSTDirectiveOption)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-rst-directive-option", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "rst:directive:option", - "is_markdown": true, - "description": [ - "Describes an option for reST directive. The name can be a single option", - "name or option name with arguments which separated with colon (`:`).", - "For example:", - "", - "```", - ".. rst:directive:: toctree", - "", - " .. rst:directive:option:: caption: caption of ToC", - "", - " .. rst:directive:option:: glob", - "```", - "", - "will be rendered as:", - "", - "", - "", - "", - "", - "", - "options", - "", - "Describe the type of option value.", - "", - "For example:", - "", - "```", - ".. rst:directive:: toctree", - "", - " .. rst:directive:option:: maxdepth", - " :type: integer or no value", - "```" - ], - "options": {} - }, - "rst:role(sphinx.domains.rst.ReSTRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-rst-role", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "rst:role", - "is_markdown": true, - "description": [ - "Describes a reST role. For example:", - "", - "```", - ".. rst:role:: foo", - "", - " Foo description.", - "```", - "", - "will be rendered as:", - "", - "", - "Foo description." - ], - "options": {} - }, - "centered(sphinx.directives.other.Centered)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-centered", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "centered", - "is_markdown": true, - "description": [ - "This directive creates a centered boldfaced line of text. Use it as", - "follows:", - "", - "```", - ".. centered:: LICENSE AGREEMENT", - "```" - ], - "options": {} - }, - "code-block(sphinx.directives.code.CodeBlock)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "code-block", - "is_markdown": true, - "description": [ - "Example:", - "", - "```", - ".. code-block:: ruby", - "", - " Some Ruby code.", - "```", - "", - "The directive's alias name [sourcecode](https://www.sphinx-doc.org/en/master/_sources/usage/restructuredtext/directives.rst.txt#None) works as well. This", - "directive takes a language name as an argument. It can be [any lexer alias", - "supported by Pygments](https://pygments.org/docs/lexers/). If it is not", - "given, the setting of [highlight](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-highlight) directive will be used.", - "If not set, [highlight_language](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-highlight_language) will be used.", - "", - "", - "options", - "", - "Enable to generate line numbers for the code block:", - "", - "```", - ".. code-block:: ruby", - " :linenos:", - "", - " Some more Ruby code.", - "```", - "", - "", - "", - "Set the first line number of the code block. If present, `linenos`", - "option is also automatically activated:", - "", - "```", - ".. code-block:: ruby", - " :lineno-start: 10", - "", - " Some more Ruby code, with line numbering starting at 10.", - "```", - "", - "", - "", - "", - "", - "Emphasize particular lines of the code block:", - "", - "```", - ".. code-block:: python", - " :emphasize-lines: 3,5", - "", - " def some_function():", - " interesting = False", - " print 'This line is highlighted.'", - " print 'This one is not...'", - " print '...but this one is.'", - "```", - "", - "", - "", - "", - "", - "rst:directive:option: force", - ":type: no value", - "", - "Ignore minor errors on highlighting", - "", - ".. versionchanged:: 2.1", - "", - "Set a caption to the code block.", - "", - "", - "", - "", - "", - "Define implicit target name that can be referenced by using", - "[ref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref). For example:", - "", - "```", - ".. code-block:: python", - " :caption: this.py", - " :name: this-py", - "", - " print 'Explicit is better than implicit.'", - "```", - "", - "In order to cross-reference a code-block using either the", - "[ref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref) or the [numref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-numref) role, it is necessary", - "that both name and caption be defined. The", - "argument of name can then be given to [numref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-numref)", - "to generate the cross-reference. Example:", - "", - "```", - "See :numref:`this-py` for an example.", - "```", - "", - "When using [ref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref), it is possible to generate a cross-reference", - "with only name defined, provided an explicit title is", - "given. Example:", - "", - "```", - "See :ref:`this code snippet <this-py>` for an example.", - "```", - "", - "", - "", - "", - "", - "Strip indentation characters from the code block. When number given,", - "leading N characters are removed. When no argument given, leading spaces", - "are removed via [textwrap.dedent()](https://www.sphinx-doc.org/en/master/_sources/usage/restructuredtext/directives.rst.txt#None). For example:", - "", - "```", - ".. code-block:: ruby", - " :linenos:", - " :dedent: 4", - "", - " some ruby code", - "```", - "", - "", - "", - "", - "", - "", - "", - "If given, minor errors on highlighting are ignored." - ], - "options": {} - }, - "codeauthor(sphinx.directives.other.Author)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-codeauthor", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "codeauthor", - "is_markdown": true, - "description": [ - "The [codeauthor](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-codeauthor) directive, which can appear multiple times, names", - "the authors of the described code, just like [sectionauthor](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-sectionauthor) names", - "the author(s) of a piece of documentation. It too only produces output if", - "the [show_authors](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-show_authors) configuration value is `True`." - ], - "options": {} - }, - "deprecated(sphinx.domains.changeset.VersionChange)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-deprecated", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "deprecated", - "is_markdown": true, - "description": [ - "Similar to [versionchanged](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-versionchanged), but describes when the feature was", - "deprecated. An explanation can also be given, for example to inform the", - "reader what should be used instead. Example:", - "", - "```", - ".. deprecated:: 3.1", - " Use :func:`spam` instead.", - "```" - ], - "options": {} - }, - "glossary(sphinx.domains.std.Glossary)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-glossary", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "glossary", - "is_markdown": true, - "description": [ - "This directive must contain a reST definition-list-like markup with terms and", - "definitions. The definitions will then be referenceable with the", - "[term](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-term) role. Example:", - "", - "```", - ".. glossary::", - "", - " environment", - " A structure where information about all documents under the root is", - " saved, and used for cross-referencing. The environment is pickled", - " after the parsing stage, so that successive runs only need to read", - " and parse new and changed documents.", - "", - " source directory", - " The directory which, including its subdirectories, contains all", - " source files for one Sphinx project.", - "```", - "", - "In contrast to regular definition lists, multiple terms per entry are", - "allowed, and inline markup is allowed in terms. You can link to all of the", - "terms. For example:", - "", - "```", - ".. glossary::", - "", - " term 1", - " term 2", - " Definition of both terms.", - "```", - "", - "(When the glossary is sorted, the first term determines the sort order.)", - "", - "If you want to specify \"grouping key\" for general index entries, you can put", - "a \"key\" as \"term : key\". For example:", - "", - "```", - ".. glossary::", - "", - " term 1 : A", - " term 2 : B", - " Definition of both terms.", - "```", - "", - "Note that \"key\" is used for grouping key as is.", - "The \"key\" isn't normalized; key \"A\" and \"a\" become different groups.", - "The whole characters in \"key\" is used instead of a first character; it is", - "used for \"Combining Character Sequence\" and \"Surrogate Pairs\" grouping key.", - "", - "In i18n situation, you can specify \"localized term : key\" even if original", - "text only have \"term\" part. In this case, translated \"localized term\" will be", - "categorized in \"key\" group." - ], - "options": {} - }, - "highlight(sphinx.directives.code.Highlight)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-highlight", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "highlight", - "is_markdown": true, - "description": [ - "Example:", - "", - "```", - ".. highlight:: c", - "```", - "", - "This language is used until the next `highlight` directive is encountered.", - "As discussed previously, language can be any lexer alias supported by", - "Pygments.", - "options", - "", - "Enable to generate line numbers for code blocks.", - "", - "This option takes an optional number as threshold parameter. If any", - "threshold given, the directive will produce line numbers only for the code", - "blocks longer than N lines. If not given, line numbers will be produced", - "for all of code blocks.", - "", - "Example:", - "", - "```", - ".. highlight:: python", - " :linenothreshold: 5", - "```", - "", - "", - "", - "If given, minor errors on highlighting are ignored." - ], - "options": {} - }, - "hlist(sphinx.directives.other.HList)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-hlist", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "hlist", - "is_markdown": true, - "description": [ - "This directive must contain a bullet list. It will transform it into a more", - "compact list by either distributing more than one item horizontally, or", - "reducing spacing between items, depending on the builder.", - "", - "For builders that support the horizontal distribution, there is a `columns`", - "option that specifies the number of columns; it defaults to 2. Example:", - "", - "```", - ".. hlist::", - " :columns: 3", - "", - " * A list of", - " * short items", - " * that should be", - " * displayed", - " * horizontally", - "```" - ], - "options": {} - }, - "index(sphinx.domains.index.IndexDirective)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-index", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "index", - "is_markdown": true, - "description": [ - "This directive contains one or more index entries. Each entry consists of a", - "type and a value, separated by a colon.", - "", - "For example:", - "", - "```", - ".. index::", - " single: execution; context", - " module: __main__", - " module: sys", - " triple: module; search; path", - "", - "The execution context", - "---------------------", - "", - "...", - "```", - "", - "This directive contains five entries, which will be converted to entries in", - "the generated index which link to the exact location of the index statement", - "(or, in case of offline media, the corresponding page number).", - "", - "Since index directives generate cross-reference targets at their location in", - "the source, it makes sense to put them before the thing they refer to --", - "e.g. a heading, as in the example above.", - "", - "The possible entry types are:", - "", - "`single`: ", - "Creates a single index entry. Can be made a subentry by separating the", - "subentry text with a semicolon (this notation is also used below to", - "describe what entries are created).", - "", - "`pair`: ", - "`pair: loop; statement` is a shortcut that creates two index entries,", - "namely `loop; statement` and `statement; loop`.", - "", - "`triple`: ", - "Likewise, `triple: module; search; path` is a shortcut that creates", - "three index entries, which are `module; search path`, `search; path,", - "module` and `path; module search`.", - "", - "`see`: ", - "`see: entry; other` creates an index entry that refers from `entry` to", - "`other`.", - "", - "`seealso`: ", - "Like `see`, but inserts \"see also\" instead of \"see\".", - "", - "`module, keyword, operator, object, exception, statement, builtin`: ", - "These all create two index entries. For example, `module: hashlib`", - "creates the entries `module; hashlib` and `hashlib; module`. (These", - "are Python-specific and therefore deprecated.)", - "", - "You can mark up \"main\" index entries by prefixing them with an exclamation", - "mark. The references to \"main\" entries are emphasized in the generated", - "index. For example, if two pages contain", - "", - "```", - ".. index:: Python", - "```", - "", - "and one page contains", - "", - "```", - ".. index:: ! Python", - "```", - "", - "then the backlink to the latter page is emphasized among the three backlinks.", - "", - "For index directives containing only \"single\" entries, there is a shorthand", - "notation:", - "", - "```", - ".. index:: BNF, grammar, syntax, notation", - "```", - "", - "This creates four index entries.", - "", - "", - "options", - "", - "Define implicit target name that can be referenced by using", - "[ref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref). For example:", - "", - "```", - ".. index:: Python", - " :name: py-index", - "```" - ], - "options": { - "single": "\nCreates a single index entry. Can be made a subentry by separating the\nsubentry text with a semicolon (this notation is also used below to\ndescribe what entries are created).\n", - "pair": "\n`pair: loop; statement` is a shortcut that creates two index entries,\nnamely `loop; statement` and `statement; loop`.\n", - "triple": "\nLikewise, `triple: module; search; path` is a shortcut that creates\nthree index entries, which are `module; search path`, `search; path,\nmodule` and `path; module search`.\n", - "see": "\n`see: entry; other` creates an index entry that refers from `entry` to\n`other`.\n", - "seealso": "\nLike `see`, but inserts \"see also\" instead of \"see\".\n", - "module, keyword, operator, object, exception, statement, builtin": "\nThese all create two index entries. For example, `module: hashlib`\ncreates the entries `module; hashlib` and `hashlib; module`. (These\nare Python-specific and therefore deprecated.)\n" - } - }, - "literalinclude(sphinx.directives.code.LiteralInclude)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "literalinclude", - "is_markdown": true, - "description": [ - "Longer displays of verbatim text may be included by storing the example text", - "in an external file containing only plain text. The file may be included", - "using the `literalinclude` directive. 3 For example, to include the", - "Python source file `example.py`, use:", - "", - "```", - ".. literalinclude:: example.py", - "```", - "", - "The file name is usually relative to the current file's path. However, if", - "it is absolute (starting with `/`), it is relative to the top source", - "directory.", - "", - "Additional options", - "", - "Like [code-block](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block), the directive supports the `linenos` flag", - "option to switch on line numbers, the `lineno-start` option to select the", - "first line number, the `emphasize-lines` option to emphasize particular", - "lines, the `name` option to provide an implicit target name, the", - "`dedent` option to strip indentation characters for the code block, and a", - "`language` option to select a language different from the current file's", - "standard language. In addition, it supports the `caption` option; however,", - "this can be provided with no argument to use the filename as the caption.", - "Example with options:", - "", - "```", - ".. literalinclude:: example.rb", - " :language: ruby", - " :emphasize-lines: 12,15-18", - " :linenos:", - "```", - "", - "Tabs in the input are expanded if you give a `tab-width` option with the", - "desired tab width.", - "", - "Include files are assumed to be encoded in the [source_encoding](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-source_encoding).", - "If the file has a different encoding, you can specify it with the", - "`encoding` option:", - "", - "```", - ".. literalinclude:: example.py", - " :encoding: latin-1", - "```", - "", - "The directive also supports including only parts of the file. If it is a", - "Python module, you can select a class, function or method to include using", - "the `pyobject` option:", - "", - "```", - ".. literalinclude:: example.py", - " :pyobject: Timer.start", - "```", - "", - "This would only include the code lines belonging to the `start()` method", - "in the `Timer` class within the file.", - "", - "Alternately, you can specify exactly which lines to include by giving a", - "`lines` option:", - "", - "```", - ".. literalinclude:: example.py", - " :lines: 1,3,5-10,20-", - "```", - "", - "This includes the lines 1, 3, 5 to 10 and lines 20 to the last line.", - "", - "Another way to control which part of the file is included is to use the", - "`start-after` and `end-before` options (or only one of them). If", - "`start-after` is given as a string option, only lines that follow the", - "first line containing that string are included. If `end-before` is given", - "as a string option, only lines that precede the first lines containing that", - "string are included. The `start-at` and `end-at` options behave in a", - "similar way, but the lines containing the matched string are included.", - "", - "`start-after`/`start-at` and `end-before`/`end-at` can have same string.", - "`start-after`/`start-at` filter lines before the line that contains", - "option string (`start-at` will keep the line). Then `end-before`/`end-at`", - "filter lines after the line that contains option string (`end-at` will keep", - "the line and `end-before` skip the first line).", - "", - "If you want to select only `[second-section]` of ini file like the", - "following, you can use `:start-at: [second-section]` and", - "`:end-before: [third-section]`:", - "", - "```", - "[first-section]", - "", - "var_in_first=true", - "", - "[second-section]", - "", - "var_in_second=true", - "", - "[third-section]", - "", - "var_in_third=true", - "```", - "", - "Useful cases of these option is working with tag comments.", - "`:start-after: [initialized]` and `:end-before: [initialized]` options", - "keep lines between comments:", - "", - "```", - "if __name__ == \"__main__\":", - " # [initialize]", - " app.start(\":8000\")", - " # [initialize]", - "```", - "", - "When lines have been selected in any of the ways described above, the line", - "numbers in `emphasize-lines` refer to those selected lines, counted", - "consecutively starting at `1`.", - "", - "When specifying particular parts of a file to display, it can be useful to", - "display the original line numbers. This can be done using the", - "`lineno-match` option, which is however allowed only when the selection", - "consists of contiguous lines.", - "", - "You can prepend and/or append a line to the included code, using the", - "`prepend` and `append` option, respectively. This is useful e.g. for", - "highlighting PHP code that doesn't include the `<?php`/`?>` markers.", - "", - "If you want to show the diff of the code, you can specify the old file by", - "giving a `diff` option:", - "", - "```", - ".. literalinclude:: example.py", - " :diff: example.py.orig", - "```", - "", - "This shows the diff between `example.py` and `example.py.orig` with", - "unified diff format.", - "", - "A `force` option can ignore minor errors on highlighting." - ], - "options": {} - }, - "math(sphinx.directives.patches.MathDirective)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-math", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "math", - "is_markdown": true, - "description": [ - "Directive for displayed math (math that takes the whole line for itself).", - "", - "The directive supports multiple equations, which should be separated by a", - "blank line:", - "", - "```", - ".. math::", - "", - " (a + b)^2 = a^2 + 2ab + b^2", - "", - " (a - b)^2 = a^2 - 2ab + b^2", - "```", - "", - "In addition, each single equation is set within a `split` environment,", - "which means that you can have multiple aligned lines in an equation,", - "aligned at `&` and separated by `\\\\`:", - "", - "```", - ".. math::", - "", - " (a + b)^2 &= (a + b)(a + b) \\\\", - " &= a^2 + 2ab + b^2", - "```", - "", - "For more details, look into the documentation of the [AmSMath LaTeX", - "package](https://www.ams.org/publications/authors/tex/amslatex).", - "", - "When the math is only one line of text, it can also be given as a directive", - "argument:", - "", - "```", - ".. math:: (a + b)^2 = a^2 + 2ab + b^2", - "```", - "", - "Normally, equations are not numbered. If you want your equation to get a", - "number, use the `label` option. When given, it selects an internal label", - "for the equation, by which it can be cross-referenced, and causes an equation", - "number to be issued. See [eq](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-eq) for an example. The numbering", - "style depends on the output format.", - "", - "There is also an option `nowrap` that prevents any wrapping of the given", - "math in a math environment. When you give this option, you must make sure", - "yourself that the math is properly set up. For example:", - "", - "```", - ".. math::", - " :nowrap:", - "", - " \\begin{eqnarray}", - " y & = & ax^2 + bx + c \\\\", - " f(x) & = & x^2 + 2xy + y^2", - " \\end{eqnarray}", - "```" - ], - "options": {} - }, - "note(docutils.parsers.rst.directives.admonitions.Note)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-note", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "note", - "is_markdown": true, - "description": [ - "An especially important bit of information about an API that a user should be", - "aware of when using whatever bit of API the note pertains to. The content of", - "the directive should be written in complete sentences and include all", - "appropriate punctuation.", - "", - "Example:", - "", - "```", - ".. note::", - "", - " This function is not suitable for sending spam e-mails.", - "```" - ], - "options": {} - }, - "only(sphinx.directives.other.Only)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-only", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "only", - "is_markdown": true, - "description": [ - "Include the content of the directive only if the expression is true. The", - "expression should consist of tags, like this:", - "", - "```", - ".. only:: html and draft", - "```", - "", - "Undefined tags are false, defined tags (via the `-t` command-line option or", - "within `conf.py`, see [here ](https://www.sphinx-doc.org/en/master/usage/configuration.html#conf-tags)) are true. Boolean", - "expressions, also using parentheses (like `html and (latex or draft)`) are", - "supported.", - "", - "The format and the name of the current builder (`html`, `latex` or", - "`text`) are always set as a tag 4. To make the distinction between", - "format and name explicit, they are also added with the prefix `format_` and", - "`builder_`, e.g. the epub builder defines the tags `html`, `epub`,", - "`format_html` and `builder_epub`.", - "", - "These standard tags are set after the configuration file is read, so they", - "are not available there.", - "", - "All tags must follow the standard Python identifier syntax as set out in", - "the [Identifiers and keywords](https://docs.python.org/3/reference/lexical_analysis.html#identifiers)", - "documentation. That is, a tag expression may only consist of tags that", - "conform to the syntax of Python variables. In ASCII, this consists of the", - "uppercase and lowercase letters `A` through `Z`, the underscore `_`", - "and, except for the first character, the digits `0` through `9`.", - "", - "", - "", - "", - "", - "This directive is designed to control only content of document. It could", - "not control sections, labels and so on." - ], - "options": {} - }, - "productionlist(sphinx.domains.std.ProductionList)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-productionlist", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "productionlist", - "is_markdown": true, - "description": [ - "This directive is used to enclose a group of productions. Each production", - "is given on a single line and consists of a name, separated by a colon from", - "the following definition. If the definition spans multiple lines, each", - "continuation line must begin with a colon placed at the same column as in", - "the first line.", - "Blank lines are not allowed within `productionlist` directive arguments.", - "", - "The definition can contain token names which are marked as interpreted text", - "(e.g., \"`sum ::= `integer` \"+\" `integer``\") -- this generates", - "cross-references to the productions of these tokens. Outside of the", - "production list, you can reference to token productions using", - "[token](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-token).", - "", - "The productionGroup argument to [productionlist](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-productionlist) serves to", - "distinguish different sets of production lists that belong to different", - "grammars. Multiple production lists with the same productionGroup thus", - "define rules in the same scope.", - "", - "Inside of the production list, tokens implicitly refer to productions", - "from the current group. You can refer to the production of another", - "grammar by prefixing the token with its group name and a colon, e.g,", - "\"`otherGroup:sum`\". If the group of the token should not be shown in", - "the production, it can be prefixed by a tilde, e.g.,", - "\"`~otherGroup:sum`\". To refer to a production from an unnamed", - "grammar, the token should be prefixed by a colon, e.g., \"`:sum`\".", - "", - "Outside of the production list,", - "if you have given a productionGroup argument you must prefix the", - "token name in the cross-reference with the group name and a colon,", - "e.g., \"`myGroup:sum`\" instead of just \"`sum`\".", - "If the group should not be shown in the title of the link either", - "an explicit title can be given (e.g., \"`myTitle <myGroup:sum>`\"),", - "or the target can be prefixed with a tilde (e.g., \"`~myGroup:sum`\").", - "", - "Note that no further reST parsing is done in the production, so that you", - "don't have to escape `*` or `|` characters." - ], - "options": {} - }, - "rubric(docutils.parsers.rst.directives.body.Rubric)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-rubric", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "rubric", - "is_markdown": true, - "description": [ - "This directive creates a paragraph heading that is not used to create a", - "table of contents node.", - "", - "If the title of the rubric is \"Footnotes\" (or the selected language's", - "equivalent), this rubric is ignored by the LaTeX writer, since it is", - "assumed to only contain footnote definitions and therefore would create an", - "empty heading." - ], - "options": {} - }, - "sectionauthor(sphinx.directives.other.Author)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-sectionauthor", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "sectionauthor", - "is_markdown": true, - "description": [ - "Identifies the author of the current section. The argument should include", - "the author's name such that it can be used for presentation and email", - "address. The domain name portion of the address should be lower case.", - "Example:", - "", - "```", - ".. sectionauthor:: Guido van Rossum <guido@python.org>", - "```", - "", - "By default, this markup isn't reflected in the output in any way (it helps", - "keep track of contributions), but you can set the configuration value", - "[show_authors](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-show_authors) to `True` to make them produce a paragraph in the", - "output." - ], - "options": {} - }, - "seealso(sphinx.directives.other.SeeAlso)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-seealso", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "seealso", - "is_markdown": true, - "description": [ - "Many sections include a list of references to module documentation or", - "external documents. These lists are created using the [seealso](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-seealso)", - "directive.", - "", - "The [seealso](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-seealso) directive is typically placed in a section just before", - "any subsections. For the HTML output, it is shown boxed off from the main", - "flow of the text.", - "", - "The content of the [seealso](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-seealso) directive should be a reST definition", - "list. Example:", - "", - "```", - ".. seealso::", - "", - " Module :py:mod:`zipfile`", - " Documentation of the :py:mod:`zipfile` standard module.", - "", - " `GNU tar manual, Basic Tar Format <http://link>`_", - " Documentation for tar archive files, including GNU tar extensions.", - "```", - "", - "There's also a \"short form\" allowed that looks like this:", - "", - "```", - ".. seealso:: modules :py:mod:`zipfile`, :py:mod:`tarfile`", - "```" - ], - "options": {} - }, - "tabularcolumns(sphinx.directives.other.TabularColumns)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-tabularcolumns", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "tabularcolumns", - "is_markdown": true, - "description": [ - "This directive gives a \"column spec\" for the next table occurring in the", - "source file. The spec is the second argument to the LaTeX `tabulary`", - "package's environment (which Sphinx uses to translate tables). It can have", - "values like", - "", - "```", - "|l|l|l|", - "```", - "", - "which means three left-adjusted, nonbreaking columns. For columns with", - "longer text that should automatically be broken, use either the standard", - "`p{width}` construct, or tabulary's automatic specifiers:", - "", - "`L`", - "", - "flush left column with automatic width", - "", - "`R`", - "", - "flush right column with automatic width", - "", - "`C`", - "", - "centered column with automatic width", - "", - "`J`", - "", - "justified column with automatic width", - "", - "The automatic widths of the `LRCJ` columns are attributed by `tabulary`", - "in proportion to the observed shares in a first pass where the table cells", - "are rendered at their natural \"horizontal\" widths.", - "", - "By default, Sphinx uses a table layout with `J` for every column.", - "", - "", - "", - "", - "", - "Sphinx actually uses `T` specifier having done `\\newcolumntype{T}{J}`.", - "To revert to previous default, insert `\\newcolumntype{T}{L}` in the", - "LaTeX preamble (see [latex_elements](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-latex_elements)).", - "", - "A frequent issue with tabulary is that columns with little contents are", - "\"squeezed\". The minimal column width is a tabulary parameter called", - "`\\tymin`. You may set it globally in the LaTeX preamble via", - "`\\setlength{\\tymin}{40pt}` for example.", - "", - "Else, use the [tabularcolumns](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-tabularcolumns) directive with an explicit", - "`p{40pt}` (for example) for that column. You may use also `l`", - "specifier but this makes the task of setting column widths more difficult", - "if some merged cell intersects that column.", - "", - "Tables with more than 30 rows are rendered using `longtable`, not", - "`tabulary`, in order to allow pagebreaks. The `L`, `R`, ...", - "specifiers do not work for these tables.", - "", - "Tables that contain list-like elements such as object descriptions,", - "blockquotes or any kind of lists cannot be set out of the box with", - "`tabulary`. They are therefore set with the standard LaTeX `tabular`", - "(or `longtable`) environment if you don't give a `tabularcolumns`", - "directive. If you do, the table will be set with `tabulary` but you", - "must use the `p{width}` construct (or Sphinx's `\\X` and `\\Y`", - "specifiers described below) for the columns containing these elements.", - "", - "Literal blocks do not work with `tabulary` at all, so tables containing", - "a literal block are always set with `tabular`. The verbatim environment", - "used for literal blocks only works in `p{width}` (and `\\X` or `\\Y`)", - "columns, hence Sphinx generates such column specs for tables containing", - "literal blocks.", - "", - "Since Sphinx 1.5, the `\\X{a}{b}` specifier is used (there is a backslash", - "in the specifier letter). It is like `p{width}` with the width set to a", - "fraction `a/b` of the current line width. You can use it in the", - "[tabularcolumns](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-tabularcolumns) (it is not a problem if some LaTeX macro is also", - "called `\\X`.)", - "", - "It is not needed for `b` to be the total number of columns, nor for the", - "sum of the fractions of the `\\X` specifiers to add up to one. For example", - "`|\\X{2}{5}|\\X{1}{5}|\\X{1}{5}|` is legitimate and the table will occupy", - "80% of the line width, the first of its three columns having the same width", - "as the sum of the next two.", - "", - "This is used by the `:widths:` option of the [table](https://docutils.sourceforge.io/docs/ref/rst/directives.html#table) directive.", - "", - "Since Sphinx 1.6, there is also the `\\Y{f}` specifier which admits a", - "decimal argument, such has `\\Y{0.15}`: this would have the same effect as", - "`\\X{3}{20}`.", - "", - "", - "Merged cells from complex grid tables (either multi-row, multi-column, or", - "both) now allow blockquotes, lists, literal blocks, ... as do regular", - "cells.", - "", - "Sphinx's merged cells interact well with `p{width}`, `\\X{a}{b}`,", - "`\\Y{f}` and tabulary's columns.", - "", - "", - "[tabularcolumns](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-tabularcolumns) conflicts with `:widths:` option of table", - "directives. If both are specified, `:widths:` option will be ignored." - ], - "options": {} - }, - "toctree(sphinx.directives.other.TocTree)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-toctree", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "toctree", - "is_markdown": true, - "description": [ - "This directive inserts a \"TOC tree\" at the current location, using the", - "individual TOCs (including \"sub-TOC trees\") of the documents given in the", - "directive body. Relative document names (not beginning with a slash) are", - "relative to the document the directive occurs in, absolute names are relative", - "to the source directory. A numeric `maxdepth` option may be given to", - "indicate the depth of the tree; by default, all levels are included. 1", - "", - "The representation of \"TOC tree\" is changed in each output format. The", - "builders that output multiple files (ex. HTML) treat it as a collection of", - "hyperlinks. On the other hand, the builders that output a single file (ex.", - "LaTeX, man page, etc.) replace it with the content of the documents on the", - "TOC tree.", - "", - "Consider this example (taken from the Python docs' library reference index):", - "", - "```", - ".. toctree::", - " :maxdepth: 2", - "", - " intro", - " strings", - " datatypes", - " numeric", - " (many more documents listed here)", - "```", - "", - "This accomplishes two things:", - "- Tables of contents from all those documents are inserted, with a maximum", - "depth of two, that means one nested heading. `toctree` directives in", - "those documents are also taken into account.", - "- Sphinx knows the relative order of the documents `intro`,", - "`strings` and so forth, and it knows that they are children of the shown", - "document, the library index. From this information it generates \"next", - "chapter\", \"previous chapter\" and \"parent chapter\" links.", - "", - "Entries", - "", - "Document titles in the [toctree](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-toctree) will be automatically read from the", - "title of the referenced document. If that isn't what you want, you can", - "specify an explicit title and target using a similar syntax to reST", - "hyperlinks (and Sphinx's [cross-referencing syntax ](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#xref-syntax)). This", - "looks like:", - "", - "```", - ".. toctree::", - "", - " intro", - " All about strings <strings>", - " datatypes", - "```", - "", - "The second line above will link to the `strings` document, but will use the", - "title \"All about strings\" instead of the title of the `strings` document.", - "", - "You can also add external links, by giving an HTTP URL instead of a document", - "name.", - "", - "Section numbering", - "", - "If you want to have section numbers even in HTML output, give the", - "toplevel toctree a `numbered` option. For example:", - "", - "```", - ".. toctree::", - " :numbered:", - "", - " foo", - " bar", - "```", - "", - "Numbering then starts at the heading of `foo`. Sub-toctrees are", - "automatically numbered (don't give the `numbered` flag to those).", - "", - "Numbering up to a specific depth is also possible, by giving the depth as a", - "numeric argument to `numbered`.", - "", - "Additional options", - "", - "You can use the `caption` option to provide a toctree caption and you can", - "use the `name` option to provide an implicit target name that can be", - "referenced by using [ref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref):", - "", - "```", - ".. toctree::", - " :caption: Table of Contents", - " :name: mastertoc", - "", - " foo", - "```", - "", - "If you want only the titles of documents in the tree to show up, not other", - "headings of the same level, you can use the `titlesonly` option:", - "", - "```", - ".. toctree::", - " :titlesonly:", - "", - " foo", - " bar", - "```", - "", - "You can use \"globbing\" in toctree directives, by giving the `glob` flag", - "option. All entries are then matched against the list of available", - "documents, and matches are inserted into the list alphabetically. Example:", - "", - "```", - ".. toctree::", - " :glob:", - "", - " intro*", - " recipe/*", - " *", - "```", - "", - "This includes first all documents whose names start with `intro`, then all", - "documents in the `recipe` folder, then all remaining documents (except the", - "one containing the directive, of course.) 2", - "", - "The special entry name `self` stands for the document containing the", - "toctree directive. This is useful if you want to generate a \"sitemap\" from", - "the toctree.", - "", - "You can use the `reversed` flag option to reverse the order of the entries", - "in the list. This can be useful when using the `glob` flag option to", - "reverse the ordering of the files. Example:", - "", - "```", - ".. toctree::", - " :glob:", - " :reversed:", - "", - " recipe/*", - "```", - "", - "You can also give a \"hidden\" option to the directive, like this:", - "", - "```", - ".. toctree::", - " :hidden:", - "", - " doc_1", - " doc_2", - "```", - "", - "This will still notify Sphinx of the document hierarchy, but not insert links", - "into the document at the location of the directive -- this makes sense if you", - "intend to insert these links yourself, in a different style, or in the HTML", - "sidebar.", - "", - "In cases where you want to have only one top-level toctree and hide all other", - "lower level toctrees you can add the \"includehidden\" option to the top-level", - "toctree entry:", - "", - "```", - ".. toctree::", - " :includehidden:", - "", - " doc_1", - " doc_2", - "```", - "", - "All other toctree entries can then be eliminated by the \"hidden\" option.", - "", - "In the end, all documents in the [source directory](https://www.sphinx-doc.org/en/master/glossary.html#term-source-directory) (or subdirectories)", - "must occur in some `toctree` directive; Sphinx will emit a warning if it", - "finds a file that is not included, because that means that this file will not", - "be reachable through standard navigation.", - "", - "Use [exclude_patterns](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-exclude_patterns) to explicitly exclude documents or", - "directories from building completely. Use [the \"orphan\" metadata", - "](https://www.sphinx-doc.org/en/master/usage/restructuredtext/field-lists.html#metadata) to let a document be built, but notify Sphinx that it is not", - "reachable via a toctree.", - "", - "The \"root document\" (selected by [root_doc](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-root_doc)) is the \"root\" of the TOC", - "tree hierarchy. It can be used as the documentation's main page, or as a", - "\"full table of contents\" if you don't give a `maxdepth` option." - ], - "options": {} - }, - "versionadded(sphinx.domains.changeset.VersionChange)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-versionadded", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "versionadded", - "is_markdown": true, - "description": [ - "This directive documents the version of the project which added the described", - "feature to the library or C API. When this applies to an entire module, it", - "should be placed at the top of the module section before any prose.", - "", - "The first argument must be given and is the version in question; you can add", - "a second argument consisting of a brief explanation of the change.", - "", - "Example:", - "", - "```", - ".. versionadded:: 2.5", - " The *spam* parameter.", - "```", - "", - "Note that there must be no blank line between the directive head and the", - "explanation; this is to make these blocks visually continuous in the markup." - ], - "options": {} - }, - "versionchanged(sphinx.domains.changeset.VersionChange)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-versionchanged", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "versionchanged", - "is_markdown": true, - "description": [ - "Similar to [versionadded](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-versionadded), but describes when and what changed in", - "the named feature in some way (new parameters, changed side effects, etc.)." - ], - "options": {} - }, - "warning(docutils.parsers.rst.directives.admonitions.Warning)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-warning", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "warning", - "is_markdown": true, - "description": [ - "An important bit of information about an API that a user should be very aware", - "of when using whatever bit of API the warning pertains to. The content of", - "the directive should be written in complete sentences and include all", - "appropriate punctuation. This differs from [note](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-note) in that it is", - "recommended over [note](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-note) for information regarding security." - ], - "options": {} - } -} diff --git a/lib/esbonio/esbonio/lsp/sphinx/directives.py b/lib/esbonio/esbonio/lsp/sphinx/directives.py deleted file mode 100644 index 01c58d9ee..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/directives.py +++ /dev/null @@ -1,10 +0,0 @@ -import json - -from esbonio.lsp.directives import Directives -from esbonio.lsp.sphinx import SphinxLanguageServer -from esbonio.lsp.util import resources - - -def esbonio_setup(rst: SphinxLanguageServer, directives: Directives): - sphinx_docs = resources.read_string("esbonio.lsp.sphinx", "directives.json") - directives.add_documentation(json.loads(sphinx_docs)) diff --git a/lib/esbonio/esbonio/lsp/sphinx/domains.py b/lib/esbonio/esbonio/lsp/sphinx/domains.py deleted file mode 100644 index 582ed2823..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/domains.py +++ /dev/null @@ -1,631 +0,0 @@ -"""Support for Sphinx domains.""" -import pathlib -from typing import Any -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple -from typing import Type - -import pygls.uris as Uri -from docutils import nodes -from docutils.parsers.rst import Directive -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionItemKind -from lsprotocol.types import Location -from lsprotocol.types import Position -from lsprotocol.types import Range -from pygls.workspace import Document -from sphinx.domains import Domain - -from esbonio.lsp import CompletionContext -from esbonio.lsp import DefinitionContext -from esbonio.lsp import DocumentLinkContext -from esbonio.lsp.directives import DirectiveLanguageFeature -from esbonio.lsp.directives import Directives -from esbonio.lsp.roles import RoleLanguageFeature -from esbonio.lsp.roles import Roles -from esbonio.lsp.sphinx import SphinxLanguageServer - -TARGET_KINDS = { - "attribute": CompletionItemKind.Field, - "doc": CompletionItemKind.File, - "class": CompletionItemKind.Class, - "envvar": CompletionItemKind.Variable, - "function": CompletionItemKind.Function, - "method": CompletionItemKind.Method, - "module": CompletionItemKind.Module, - "term": CompletionItemKind.Text, -} - - -class DomainHelpers: - """Common methods that work on domains.""" - - rst: SphinxLanguageServer - - @property - def domains(self) -> Dict[str, Domain]: - """Return a dictionary of known domains.""" - - if self.rst.app is None or self.rst.app.env is None: - return dict() - - return self.rst.app.env.domains # type: ignore - - def get_default_domain(self, uri: str) -> Optional[str]: - """Return the default domain for the given uri.""" - - # TODO: Add support for .. default-domain:: - if self.rst.app is not None: - return self.rst.app.config.primary_domain - - return None - - -class DomainDirectives(DirectiveLanguageFeature, DomainHelpers): - """Support for directives coming from Sphinx's domains.""" - - def __init__(self, rst: SphinxLanguageServer): - self.rst = rst - - self._directives: Optional[Dict[str, Type[Directive]]] = None - """Cache for known directives.""" - - @property - def directives(self) -> Dict[str, Type[Directive]]: - if self._directives is not None: - return self._directives - - directives = {} - for prefix, domain in self.domains.items(): - for name, directive in domain.directives.items(): - directives[f"{prefix}:{name}"] = directive - - self._directives = directives - return self._directives - - def get_implementation( - self, directive: str, domain: Optional[str] - ) -> Optional[Type[Directive]]: - if domain is not None: - return self.directives.get(f"{domain}:{directive}", None) - - if self.rst.app is None: - return None - - # Try the default domain - primary_domain = self.rst.app.config.primary_domain - impl = self.directives.get(f"{primary_domain}:{directive}", None) - if impl is not None: - return impl - - # Try the std domain - return self.directives.get(f"std:{directive}", None) - - def index_directives(self) -> Dict[str, Type[Directive]]: - return self.directives - - def suggest_directives( - self, context: CompletionContext - ) -> Iterable[Tuple[str, Type[Directive]]]: - # In addition to providing each directive fully qualified, we should provide a - # suggestion for directives in the std and primary domains without the prefix. - items = self.directives.copy() - primary_domain = self.get_default_domain(context.doc.uri) - - for key, directive in self.directives.items(): - if key.startswith("std:"): - items[key.replace("std:", "")] = directive - continue - - if primary_domain and key.startswith(f"{primary_domain}:"): - items[key.replace(f"{primary_domain}:", "")] = directive - - return items.items() - - def suggest_options( - self, context: CompletionContext, directive: str, domain: Optional[str] - ) -> Iterable[str]: - impl = self.get_implementation(directive, domain) - if impl is None: - return [] - - option_spec = getattr(impl, "option_spec", {}) - return option_spec.keys() - - -class DomainRoles(RoleLanguageFeature, DomainHelpers): - """Support for roles coming from Sphinx's domains.""" - - def __init__(self, rst: SphinxLanguageServer): - self.rst = rst - - self._roles: Optional[Dict[str, Any]] = None - """Cache for known roles.""" - - self._role_target_types: Optional[Dict[str, List[str]]] = None - """Cache for role target types.""" - - @property - def roles(self) -> Dict[str, Any]: - if self._roles is not None: - return self._roles - - roles = {} - for prefix, domain in self.domains.items(): - for name, role in domain.roles.items(): - roles[f"{prefix}:{name}"] = role - - self._roles = roles - return self._roles - - @property - def role_target_types(self) -> Dict[str, List[str]]: - if self._role_target_types is not None: - return self._role_target_types - - self._role_target_types = {} - - for prefix, domain in self.domains.items(): - fmt = "{prefix}:{name}" if prefix else "{name}" - - for name, item_type in domain.object_types.items(): - for role in item_type.roles: - role_key = fmt.format(name=role, prefix=prefix) - target_types = self._role_target_types.get(role_key, list()) - target_types.append(name) - - self._role_target_types[role_key] = target_types - - return self._role_target_types - - def _get_role_target_types(self, name: str, domain: str = "") -> List[str]: - """Return a list indicating which object types a role is capable of linking - with. - """ - - if domain: - return self.role_target_types.get(f"{domain}:{name}", []) - - # Try the primary domain - if self.rst.app and self.rst.app.config.primary_domain: - key = f"{self.rst.app.config.primary_domain}:{name}" - - if key in self.role_target_types: - return self.role_target_types[key] - - # Finally try the standard domain - return self.role_target_types.get(f"std:{name}", []) - - def _get_role_targets(self, name: str, domain: str = "") -> List[tuple]: - """Return a list of objects targeted by the given role. - - Parameters - ---------- - name: - The name of the role - domain: - The domain the role is a part of, if applicable. - """ - - targets: List[tuple] = [] - domain_obj: Optional[Domain] = None - - if domain: - domain_obj = self.domains.get(domain, None) - else: - std = self.domains.get("std", None) - if std and name in std.roles: - domain_obj = std - - elif self.rst.app and self.rst.app.config.primary_domain: - domain_obj = self.domains.get(self.rst.app.config.primary_domain, None) - - target_types = set(self._get_role_target_types(name, domain)) - - if not domain_obj: - self.rst.logger.debug( - "Unable to find domain for role '%s:%s'", domain, name - ) - return [] - - for obj in domain_obj.get_objects(): - if obj[2] in target_types: - targets.append(obj) - - return targets - - def get_implementation( - self, role: str, domain: Optional[str] - ) -> Optional[Directive]: - if domain is not None: - return self.roles.get(f"{domain}:{role}", None) - - if self.rst.app is None: - return None - - # Try the default domain - primary_domain = self.rst.app.config.primary_domain - impl = self.roles.get(f"{primary_domain}:{role}", None) - if impl is not None: - return impl - - # Try the std domain - return self.roles.get(f"std:{role}", None) - - def index_roles(self) -> Dict[str, Any]: - return self.roles - - def suggest_roles(self, context: CompletionContext) -> Iterable[Tuple[str, Any]]: - # In addition to providing each role fully qulaified, we should provide a - # suggestion for directives in the std and primary domains without the prefix. - items = self.roles.copy() - primary_domain = self.get_default_domain(context.doc.uri) - - for key, role in self.roles.items(): - if key.startswith("std:"): - items[key.replace("std:", "")] = role - continue - - if primary_domain and key.startswith(f"{primary_domain}:"): - items[key.replace(f"{primary_domain}:", "")] = role - - return items.items() - - def complete_targets( - self, context: CompletionContext, name: str, domain: str - ) -> List[CompletionItem]: - label = context.match.group("label") - - # Intersphinx targets contain ':' characters. - if ":" in label: - return [] - - return [ - object_to_completion_item(obj) - for obj in self._get_role_targets(name, domain) - ] - - def resolve_target_link( - self, context: DocumentLinkContext, name: str, domain: Optional[str], label: str - ) -> Tuple[Optional[str], Optional[str]]: - """``textDocument/documentLink`` support""" - - # Ignore intersphinx references. - if ":" in label: - return None, None - - # Other roles like :ref: do not make sense as the ``textDocument/documentLink`` - # api doesn't support specific locations like goto definition does. - if (domain is not None and domain != "std") or name != "doc": - return None, None - - return self.resolve_doc(context.doc, label), None - - def find_target_definitions( - self, context: DefinitionContext, name: str, domain: str, label: str - ) -> List[Location]: - if not domain and name == "ref": - return self.ref_definition(label) - - if not domain and name == "doc": - return self.doc_definition(context.doc, label) - - return [] - - def resolve_doc(self, doc: Document, label: str) -> Optional[str]: - if self.rst.app is None: - return None - - srcdir = self.rst.app.srcdir - currentdir = pathlib.Path(Uri.to_fs_path(doc.uri)).parent - - if label.startswith("/"): - path = pathlib.Path(srcdir, label[1:] + ".rst") - else: - path = pathlib.Path(currentdir, label + ".rst") - - if not path.exists(): - return None - - return Uri.from_fs_path(str(path)) - - def doc_definition(self, doc: Document, label: str) -> List[Location]: - """Goto definition implementation for ``:doc:`` targets""" - - uri = self.resolve_doc(doc, label) - if not uri: - return [] - - return [ - Location( - uri=uri, - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=0), - ), - ) - ] - - def ref_definition(self, label: str) -> List[Location]: - """Goto definition implementation for ``:ref:`` targets""" - - if not self.rst.app or not self.rst.app.env: - return [] - - types = set(self._get_role_target_types("ref")) - std = self.domains["std"] - if std is None: - return [] - - docname = self.find_docname_for_label(label, std, types) - if docname is None: - return [] - - path = self.rst.app.env.doc2path(docname) - uri = Uri.from_fs_path(path) - - doctree = self.rst.get_initial_doctree(uri) - if doctree is None: - return [] - - uri = None - line = None - - for node in doctree.traverse(condition=nodes.target): - if "refid" not in node: - continue - - if doctree.nameids.get(label, "") == node["refid"]: - uri = Uri.from_fs_path(node.source) - line = node.line - break - - if uri is None or line is None: - return [] - - return [ - Location( - uri=uri, - range=Range( - start=Position(line=line - 1, character=0), - end=Position(line=line, character=0), - ), - ) - ] - - def find_docname_for_label( - self, label: str, domain: Domain, types: Optional[Set[str]] = None - ) -> Optional[str]: - """Given the label name and domain it belongs to, return the docname its - definition resides in. - - Parameters - ---------- - label - The label to search for - - domain - The domain to search within - - types - A collection of object types that the label chould have. - """ - - docname = None - types = types or set() - - # _, title, _, _, anchor, priority - for name, _, type_, doc, _, _ in domain.get_objects(): - if types and type_ not in types: - continue - - if name == label: - docname = doc - break - - return docname - - -class Intersphinx(RoleLanguageFeature): - def __init__(self, rst: SphinxLanguageServer, domain: DomainRoles): - self.rst = rst - self.domain = domain - - def complete_targets( - self, context: CompletionContext, name: str, domain: str - ) -> List[CompletionItem]: - label = context.match.group("label") - - # Intersphinx targets contain ':' characters. - if ":" in label: - return self.complete_intersphinx_targets(name, domain, label) - - return self.complete_intersphinx_projects(name, domain) - - def complete_intersphinx_projects( - self, name: str, domain: str - ) -> List[CompletionItem]: - items = [] - for project in self.get_intersphinx_projects(): - if not self.has_intersphinx_targets(project, name, domain): - continue - - items.append( - CompletionItem( - label=project, detail="intersphinx", kind=CompletionItemKind.Module - ) - ) - - return items - - def complete_intersphinx_targets( - self, name: str, domain: str, label: str - ) -> List[CompletionItem]: - items = [] - project, *_ = label.split(":") - intersphinx_targets = self.get_intersphinx_targets(project, name, domain) - - for type_, targets in intersphinx_targets.items(): - items += [ - intersphinx_target_to_completion_item(project, label, target, type_) - for label, target in targets.items() - ] - - return items - - def resolve_target_link( - self, context: DocumentLinkContext, name: str, domain: Optional[str], label: str - ) -> Tuple[Optional[str], Optional[str]]: - if not self.rst.app: - return None, None - - project, *parts = label.split(":") - label = ":".join(parts) - targets = self.get_intersphinx_targets(project, name, domain or "") - - for _, items in targets.items(): - if label in items: - source, version, url, display = items[label] - name = label if display == "-" else display - tooltip = f"{name} - {source} v{version}" - - return url, tooltip - - return None, None - - def get_intersphinx_projects(self) -> List[str]: - """Return the list of configured intersphinx project names.""" - - if self.rst.app is None: - return [] - - inv = getattr(self.rst.app.env, "intersphinx_named_inventory", {}) - return list(inv.keys()) - - def has_intersphinx_targets( - self, project: str, name: str, domain: str = "" - ) -> bool: - """Return ``True`` if the given intersphinx project has targets targeted by the - given role. - - Parameters - ---------- - project - The project to check - - name - The name of the role - - domain - The domain the role is a part of, if applicable. - """ - targets = self.get_intersphinx_targets(project, name, domain) - - if len(targets) == 0: - return False - - return any([len(items) > 0 for items in targets.values()]) - - def get_intersphinx_targets( - self, project: str, name: str, domain: str = "" - ) -> Dict[str, Dict[str, tuple]]: - """Return the intersphinx objects targeted by the given role. - - Parameters - ---------- - project - The project to return targets from - - name - The name of the role - - domain - The domain the role is a part of, if applicable. - """ - - if self.rst.app is None: - return {} - - inv = getattr(self.rst.app.env, "intersphinx_named_inventory", {}) - if project not in inv: - return {} - - targets = {} - inv = inv[project] - - for target_type in self.domain._get_role_target_types(name, domain): - explicit_domain = f"{domain}:{target_type}" - if explicit_domain in inv: - targets[target_type] = inv[explicit_domain] - continue - - primary_domain = f'{self.rst.app.config.primary_domain or ""}:{target_type}' - if primary_domain in inv: - targets[target_type] = inv[primary_domain] - continue - - std_domain = f"std:{target_type}" - if std_domain in inv: - targets[target_type] = inv[std_domain] - - return targets - - -def intersphinx_target_to_completion_item( - project: str, label: str, target: tuple, type_: str -) -> CompletionItem: - # _. _. url, _ - source, version, _, display = target - - display_name = label if display == "-" else display - completion_kind = ":".join(type_.split(":")[1:]) if ":" in type_ else type_ - - if version: - version = f" v{version}" - - return CompletionItem( - label=label, - detail=f"{display_name} - {source}{version}", - kind=TARGET_KINDS.get(completion_kind, CompletionItemKind.Reference), - insert_text=f"{project}:{label}", - ) - - -def object_to_completion_item(object_: tuple) -> CompletionItem: - # _, _, _, docname, anchor, priority - name, display_name, type_, _, _, _ = object_ - insert_text = name - - key = type_.split(":")[1] if ":" in type_ else type_ - kind = TARGET_KINDS.get(key, CompletionItemKind.Reference) - - # ensure :doc: targets are inserted as an absolute path - that way the reference - # will always work regardless of the file's location. - if type_ == "doc": - insert_text = f"/{name}" - - # :option: targets need to be inserted as `<progname> <option>` in order to resolve - # correctly. However, this only seems to be the case "locally" as - # `<progname>.<option>` seems to resolve fine when using intersphinx... - if type_ == "cmdoption": - name = " ".join(name.split(".")) - display_name = name - insert_text = name - - return CompletionItem( - label=name, kind=kind, detail=str(display_name), insert_text=insert_text - ) - - -def esbonio_setup(rst: SphinxLanguageServer, directives: Directives, roles: Roles): - directives.add_feature(DomainDirectives(rst)) - - domain_roles = DomainRoles(rst) - intersphinx = Intersphinx(rst, domain_roles) - - roles.add_feature(domain_roles) - roles.add_feature(intersphinx) diff --git a/lib/esbonio/esbonio/lsp/sphinx/images.py b/lib/esbonio/esbonio/lsp/sphinx/images.py deleted file mode 100644 index e83c05654..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/images.py +++ /dev/null @@ -1,114 +0,0 @@ -import os.path -import pathlib -from typing import List -from typing import Optional -from typing import Tuple - -import pygls.uris as Uri -from lsprotocol.types import CompletionItem -from lsprotocol.types import Location -from lsprotocol.types import Position -from lsprotocol.types import Range -from pygls.workspace import Document - -from esbonio.lsp.directives import Directives -from esbonio.lsp.rst import CompletionContext -from esbonio.lsp.rst import DefinitionContext -from esbonio.lsp.rst import DocumentLinkContext -from esbonio.lsp.sphinx import SphinxLanguageServer -from esbonio.lsp.util.filepaths import complete_sphinx_filepaths -from esbonio.lsp.util.filepaths import path_to_completion_item - - -class Images: - def __init__(self, rst: SphinxLanguageServer): - self.rst = rst - self.logger = rst.logger.getChild(self.__class__.__name__) - - def complete_arguments( - self, context: CompletionContext, domain: str, name: str - ) -> List[CompletionItem]: - if domain or name not in {"figure", "image"}: - return [] - - if not self.rst.app: - return [] - - srcdir = self.rst.app.srcdir - partial = context.match.group("argument") - base = os.path.dirname(Uri.to_fs_path(context.doc.uri)) - items = complete_sphinx_filepaths(srcdir, base, partial) # type: ignore[arg-type] - - return [path_to_completion_item(context, p) for p in items] - - def find_definitions( - self, - context: DefinitionContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> List[Location]: - if domain or directive not in {"figure", "image"}: - return [] - - uri = self.resolve_path(context.doc, argument) - if not uri: - return [] - - return [ - Location( - uri=uri, - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=0), - ), - ) - ] - - def resolve_link( - self, - context: DocumentLinkContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> Tuple[Optional[str], Optional[str]]: - if domain or directive not in {"figure", "image"}: - return None, None - - if argument.startswith("https://") or argument.startswith("http://"): - return argument, None - - return self.resolve_path(context.doc, argument), None - - def resolve_path(self, doc: Document, argument: str) -> Optional[str]: - if argument.startswith("/"): - if not self.rst.app: - return None - - basedir = pathlib.Path(self.rst.app.srcdir) - - # Remove the leading '/' otherwise is will wipe out the basedir when - # concatenated - argument = argument[1:] - - else: - basedir = pathlib.Path(Uri.to_fs_path(doc.uri)).parent - - try: - fullpath = basedir / argument - fpath = fullpath.resolve() - - if fpath.exists(): - return Uri.from_fs_path(str(fpath)) - except Exception: - self.logger.debug("Unable to resolve filepath '%s'", fullpath) - - return None - - -def esbonio_setup(rst: SphinxLanguageServer, directives: Directives): - images = Images(rst) - - directives.add_argument_definition_provider(images) - directives.add_argument_completion_provider(images) - directives.add_argument_link_provider(images) diff --git a/lib/esbonio/esbonio/lsp/sphinx/includes.py b/lib/esbonio/esbonio/lsp/sphinx/includes.py deleted file mode 100644 index 922d8610a..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/includes.py +++ /dev/null @@ -1,106 +0,0 @@ -import os.path -import pathlib -from typing import List -from typing import Optional -from typing import Tuple - -import pygls.uris as Uri -from lsprotocol.types import CompletionItem -from lsprotocol.types import Location -from lsprotocol.types import Position -from lsprotocol.types import Range -from pygls.workspace import Document - -from esbonio.lsp.directives import Directives -from esbonio.lsp.rst import CompletionContext -from esbonio.lsp.rst import DefinitionContext -from esbonio.lsp.rst import DocumentLinkContext -from esbonio.lsp.sphinx import SphinxLanguageServer -from esbonio.lsp.util.filepaths import complete_sphinx_filepaths -from esbonio.lsp.util.filepaths import path_to_completion_item - - -class Includes: - def __init__(self, rst: SphinxLanguageServer): - self.rst = rst - self.logger = rst.logger.getChild(self.__class__.__name__) - - def complete_arguments( - self, context: CompletionContext, domain: str, name: str - ) -> List[CompletionItem]: - if domain or name not in {"include", "literalinclude"}: - return [] - - if not self.rst.app: - return [] - - srcdir = self.rst.app.srcdir - partial = context.match.group("argument") - base = os.path.dirname(Uri.to_fs_path(context.doc.uri)) - items = complete_sphinx_filepaths(srcdir, base, partial) # type: ignore[arg-type] - - return [path_to_completion_item(context, p) for p in items] - - def find_definitions( - self, - context: DefinitionContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> List[Location]: - if domain or directive not in {"literalinclude", "include"}: - return [] - - uri = self.resolve_path(context.doc, argument) - if not uri: - return [] - - return [ - Location( - uri=uri, - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=0), - ), - ) - ] - - def resolve_link( - self, - context: DocumentLinkContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> Tuple[Optional[str], Optional[str]]: - if domain or directive not in {"literalinclude", "include"}: - return None, None - - return self.resolve_path(context.doc, argument), None - - def resolve_path(self, doc: Document, argument: str) -> Optional[str]: - if argument.startswith("/"): - if not self.rst.app: - return None - - basedir = pathlib.Path(self.rst.app.srcdir) - - # Remove the leading '/' otherwise is will wipe out the basedir when - # concatenated - argument = argument[1:] - - else: - basedir = pathlib.Path(Uri.to_fs_path(doc.uri)).parent - - fpath = (basedir / argument).resolve() - if not fpath.exists(): - return None - - return Uri.from_fs_path(str(fpath)) - - -def esbonio_setup(rst: SphinxLanguageServer, directives: Directives): - includes = Includes(rst) - - directives.add_argument_definition_provider(includes) - directives.add_argument_completion_provider(includes) - directives.add_argument_link_provider(includes) diff --git a/lib/esbonio/esbonio/lsp/sphinx/line_number_transform.py b/lib/esbonio/esbonio/lsp/sphinx/line_number_transform.py deleted file mode 100644 index c2ba06d0d..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/line_number_transform.py +++ /dev/null @@ -1,16 +0,0 @@ -from docutils import nodes -from docutils.transforms import Transform - - -class LineNumberTransform(Transform): - default_priority = 500 - - def apply(self, **kwargs): - for node in self.document.traverse(nodes.paragraph): - if node.line: - node["classes"].append("linemarker") - node["classes"].append(f"linemarker-{node.line}") - - -def setup(app): - app.add_transform(LineNumberTransform) diff --git a/lib/esbonio/esbonio/lsp/sphinx/preview.py b/lib/esbonio/esbonio/lsp/sphinx/preview.py deleted file mode 100644 index dbdd84f97..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/preview.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -from functools import partial -from http.server import HTTPServer -from http.server import SimpleHTTPRequestHandler -from multiprocessing import Queue -from typing import Any -from typing import Type - -try: - from http.server import ThreadingHTTPServer - - ServerClass: Type[HTTPServer] = ThreadingHTTPServer -except ImportError: - # ThreadingHTTPServer is only availble in Python 3.7+ - ServerClass = HTTPServer - -logger = logging.getLogger(__name__) - - -class RequestHandler(SimpleHTTPRequestHandler): - def log_message(self, format: str, *args: Any) -> None: - return logger.debug(format, *args) - - -def make_preview_server(directory: str) -> HTTPServer: - """Construst a http server that can be used to preview the docs.""" - handler_class = partial(RequestHandler, directory=directory) - return ServerClass(("localhost", 0), handler_class) - - -def start_preview_server(q: Queue, directory: str): - """Start a preview server in the given directory. - - The server's port number will be sent back via the given ``q`` object. - """ - - handler_class = partial(RequestHandler, directory=directory) - server = ServerClass(("localhost", 0), handler_class) - q.put(server.server_port) - - server.serve_forever() diff --git a/lib/esbonio/esbonio/lsp/sphinx/roles.json b/lib/esbonio/esbonio/lsp/sphinx/roles.json deleted file mode 100644 index 633be74be..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/roles.json +++ /dev/null @@ -1,1076 +0,0 @@ -{ - "abbr(sphinx.roles.Abbreviation)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-abbr", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "abbr", - "is_markdown": true, - "description": [ - "An abbreviation. If the role content contains a parenthesized explanation,", - "it will be treated specially: it will be shown in a tool-tip in HTML, and", - "output only once in LaTeX.", - "", - "Example: `:abbr:`LIFO (last-in, first-out)``." - ], - "options": {} - }, - "any(sphinx.roles.AnyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-any", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "any", - "is_markdown": true, - "description": [ - "This convenience role tries to do its best to find a valid target for its", - "reference text.", - "- First, it tries standard cross-reference targets that would be referenced", - "by [doc](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-doc), [ref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref) or [option](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-option).", - "Custom objects added to the standard domain by extensions (see", - "[.Sphinx.add_object_type](https://www.sphinx-doc.org/en/master/_sources/usage/restructuredtext/roles.rst.txt#None)) are also searched.", - "- Then, it looks for objects (targets) in all loaded domains. It is up to", - "the domains how specific a match must be. For example, in the Python", - "domain a reference of `:any:`Builder`` would match the", - "`sphinx.builders.Builder` class.", - "", - "If none or multiple targets are found, a warning will be emitted. In the", - "case of multiple targets, you can change \"any\" to a specific role.", - "", - "This role is a good candidate for setting [default_role](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-default_role). If you", - "do, you can write cross-references without a lot of markup overhead. For", - "example, in this Python function documentation", - "", - "```", - ".. function:: install()", - "", - " This function installs a `handler` for every signal known by the", - " `signal` module. See the section `about-signals` for more information.", - "```", - "", - "there could be references to a glossary term (usually `:term:`handler``), a", - "Python module (usually `:py:mod:`signal`` or `:mod:`signal``) and a", - "section (usually `:ref:`about-signals``).", - "", - "The [any](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-any) role also works together with the", - "[sphinx.ext.intersphinx](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#module-sphinx.ext.intersphinx) extension: when no local cross-reference is", - "found, all object types of intersphinx inventories are also searched." - ], - "options": {} - }, - "command(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-command", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "command", - "is_markdown": true, - "description": [ - "The name of an OS-level command, such as `rm`." - ], - "options": {} - }, - "dfn(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-dfn", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "dfn", - "is_markdown": true, - "description": [ - "Mark the defining instance of a term in the text. (No index entries are", - "generated.)" - ], - "options": {} - }, - "doc(sphinx.roles.XRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-doc", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "doc", - "is_markdown": true, - "description": [ - "Link to the specified document; the document name can be specified in", - "absolute or relative fashion. For example, if the reference", - "`:doc:`parrot`` occurs in the document `sketches/index`, then the link", - "refers to `sketches/parrot`. If the reference is `:doc:`/people`` or", - "`:doc:`../people``, the link refers to `people`.", - "", - "If no explicit link text is given (like usual: `:doc:`Monty Python members", - "</people>``), the link caption will be the title of the given document." - ], - "options": {} - }, - "download(sphinx.roles.XRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-download", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "download", - "is_markdown": true, - "description": [ - "This role lets you link to files within your source tree that are not reST", - "documents that can be viewed, but files that can be downloaded.", - "", - "When you use this role, the referenced file is automatically marked for", - "inclusion in the output when building (obviously, for HTML output only).", - "All downloadable files are put into a `_downloads/<unique hash>/`", - "subdirectory of the output directory; duplicate filenames are handled.", - "", - "An example:", - "", - "```", - "See :download:`this example script <../example.py>`.", - "```", - "", - "The given filename is usually relative to the directory the current source", - "file is contained in, but if it absolute (starting with `/`), it is taken", - "as relative to the top source directory.", - "", - "The `example.py` file will be copied to the output directory, and a", - "suitable link generated to it.", - "", - "Not to show unavailable download links, you should wrap whole paragraphs that", - "have this role:", - "", - "```", - ".. only:: builder_html", - "", - " See :download:`this example script <../example.py>`.", - "```" - ], - "options": {} - }, - "envvar(sphinx.domains.std.EnvVarXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-envvar", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "envvar", - "is_markdown": true, - "description": [ - "An environment variable. Index entries are generated. Also generates a link", - "to the matching [envvar](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-envvar) directive, if it exists." - ], - "options": {} - }, - "eq(sphinx.domains.math.MathReferenceRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-eq", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "eq", - "is_markdown": true, - "description": [ - "Same as [math:numref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-math-numref)." - ], - "options": {} - }, - "file(sphinx.roles.EmphasizedLiteral)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-file", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "file", - "is_markdown": true, - "description": [ - "The name of a file or directory. Within the contents, you can use curly", - "braces to indicate a \"variable\" part, for example:", - "", - "```", - "... is installed in :file:`/usr/lib/python2.{x}/site-packages` ...", - "```", - "", - "In the built documentation, the `x` will be displayed differently to", - "indicate that it is to be replaced by the Python minor version." - ], - "options": {} - }, - "guilabel(sphinx.roles.GUILabel)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-guilabel", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "guilabel", - "is_markdown": true, - "description": [ - "Labels presented as part of an interactive user interface should be marked", - "using `guilabel`. This includes labels from text-based interfaces such as", - "those created using [curses](https://www.sphinx-doc.org/en/master/_sources/usage/restructuredtext/roles.rst.txt#None) or other text-based libraries. Any label", - "used in the interface should be marked with this role, including button", - "labels, window titles, field names, menu and menu selection names, and even", - "values in selection lists." - ], - "options": {} - }, - "kbd(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-kbd", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "kbd", - "is_markdown": true, - "description": [ - "Mark a sequence of keystrokes. What form the key sequence takes may depend", - "on platform- or application-specific conventions. When there are no", - "relevant conventions, the names of modifier keys should be spelled out, to", - "improve accessibility for new users and non-native speakers. For example,", - "an xemacs key sequence may be marked like `:kbd:`C-x C-f``, but without", - "reference to a specific application or platform, the same sequence should be", - "marked as `:kbd:`Control-x Control-f``." - ], - "options": {} - }, - "keyword(sphinx.roles.XRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-keyword", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "keyword", - "is_markdown": true, - "description": [ - "The name of a keyword in Python. This creates a link to a reference label", - "with that name, if it exists." - ], - "options": {} - }, - "mailheader(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-mailheader", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "mailheader", - "is_markdown": true, - "description": [ - "The name of an RFC 822-style mail header. This markup does not imply that", - "the header is being used in an email message, but can be used to refer to", - "any header of the same \"style.\" This is also used for headers defined by", - "the various MIME specifications. The header name should be entered in the", - "same way it would normally be found in practice, with the camel-casing", - "conventions being preferred where there is more than one common usage. For", - "example: `:mailheader:`Content-Type``." - ], - "options": {} - }, - "makevar(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-makevar", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "makevar", - "is_markdown": true, - "description": [ - "The name of a make variable." - ], - "options": {} - }, - "manpage(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-manpage", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "manpage", - "is_markdown": true, - "description": [ - "A reference to a Unix manual page including the section, e.g.", - "`:manpage:`ls(1)``. Creates a hyperlink to an external site rendering the", - "manpage if [manpages_url](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-manpages_url) is defined." - ], - "options": {} - }, - "math(docutils.parsers.rst.roles.math_role)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-math", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "math", - "is_markdown": true, - "description": [ - "Role for inline math. Use like this:", - "", - "```", - "Since Pythagoras, we know that :math:`a^2 + b^2 = c^2`.", - "```" - ], - "options": {} - }, - "menuselection(sphinx.roles.MenuSelection)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-menuselection", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "menuselection", - "is_markdown": true, - "description": [ - "Menu selections should be marked using the `menuselection` role. This is", - "used to mark a complete sequence of menu selections, including selecting", - "submenus and choosing a specific operation, or any subsequence of such a", - "sequence. The names of individual selections should be separated by", - "`-->`.", - "", - "For example, to mark the selection \"Start > Programs\", use this markup:", - "", - "```", - ":menuselection:`Start --> Programs`", - "```", - "", - "When including a selection that includes some trailing indicator, such as", - "the ellipsis some operating systems use to indicate that the command opens a", - "dialog, the indicator should be omitted from the selection name.", - "", - "`menuselection` also supports ampersand accelerators just like", - "[guilabel](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-guilabel)." - ], - "options": {} - }, - "mimetype(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-mimetype", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "mimetype", - "is_markdown": true, - "description": [ - "The name of a MIME type, or a component of a MIME type (the major or minor", - "portion, taken alone)." - ], - "options": {} - }, - "newsgroup(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-newsgroup", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "newsgroup", - "is_markdown": true, - "description": [ - "The name of a Usenet newsgroup." - ], - "options": {} - }, - "numref(sphinx.roles.XRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-numref", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "numref", - "is_markdown": true, - "description": [ - "Link to the specified figures, tables, code-blocks and sections; the standard", - "reST labels are used. When you use this role, it will insert a reference to", - "the figure with link text by its figure number like \"Fig. 1.1\".", - "", - "If an explicit link text is given (as usual: `:numref:`Image of Sphinx (Fig.", - "%s) <my-figure>``), the link caption will serve as title of the reference.", - "As placeholders, %s and {number} get replaced by the figure", - "number and {name} by the figure caption.", - "If no explicit link text is given, the [numfig_format](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-numfig_format) setting is", - "used as fall-back default.", - "", - "If [numfig](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-numfig) is `False`, figures are not numbered,", - "so this role inserts not a reference but the label or the link text." - ], - "options": {} - }, - "option(sphinx.domains.std.OptionXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-option", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "option", - "is_markdown": true, - "description": [ - "A command-line option to an executable program. This generates a link to", - "a [option](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#directive-option) directive, if it exists." - ], - "options": {} - }, - "pep(sphinx.roles.PEP)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-pep", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "pep", - "is_markdown": true, - "description": [ - "A reference to a Python Enhancement Proposal. This generates appropriate", - "index entries. The text \"PEP number\" is generated; in the HTML output,", - "this text is a hyperlink to an online copy of the specified PEP. You can", - "link to a specific section by saying `:pep:`number#anchor``." - ], - "options": {} - }, - "program(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-program", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "program", - "is_markdown": true, - "description": [ - "The name of an executable program. This may differ from the file name for", - "the executable for some platforms. In particular, the `.exe` (or other)", - "extension should be omitted for Windows programs." - ], - "options": {} - }, - "ref(sphinx.roles.XRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "ref", - "is_markdown": true, - "description": [ - "To support cross-referencing to arbitrary locations in any document, the", - "standard reST labels are used. For this to work label names must be unique", - "throughout the entire documentation. There are two ways in which you can", - "refer to labels:", - "- If you place a label directly before a section title, you can reference to", - "it with `:ref:`label-name``. For example:", - "", - "```", - ".. _my-reference-label:", - "", - "Section to cross-reference", - "--------------------------", - "", - "This is the text of the section.", - "", - "It refers to the section itself, see :ref:`my-reference-label`.", - "```", - "The `:ref:` role would then generate a link to the section, with the", - "link title being \"Section to cross-reference\". This works just as well", - "when section and reference are in different source files.", - "Automatic labels also work with figures. For example:", - "", - "```", - ".. _my-figure:", - "", - ".. figure:: whatever", - "", - " Figure caption", - "```", - "In this case, a reference `:ref:`my-figure`` would insert a reference", - "to the figure with link text \"Figure caption\".", - "The same works for tables that are given an explicit caption using the", - "[table](https://docutils.sourceforge.io/docs/ref/rst/directives.html#table) directive.", - "- Labels that aren't placed before a section title can still be referenced,", - "but you must give the link an explicit title, using this syntax:", - "`:ref:`Link title <label-name>``.", - "", - "Reference labels must start with an underscore. When referencing a label,", - "the underscore must be omitted (see examples above).", - "", - "Using [ref](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref) is advised over standard reStructuredText links to", - "sections (like ``Section title`_`) because it works across files, when", - "section headings are changed, will raise warnings if incorrect, and works", - "for all builders that support cross-references." - ], - "options": {} - }, - "regexp(docutils.parsers.rst.roles.CustomRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-regexp", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "regexp", - "is_markdown": true, - "description": [ - "A regular expression. Quotes should not be included." - ], - "options": {} - }, - "rfc(sphinx.roles.RFC)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-rfc", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "rfc", - "is_markdown": true, - "description": [ - "A reference to an Internet Request for Comments. This generates appropriate", - "index entries. The text \"RFC number\" is generated; in the HTML output,", - "this text is a hyperlink to an online copy of the specified RFC. You can", - "link to a specific section by saying `:rfc:`number#anchor``." - ], - "options": {} - }, - "samp(sphinx.roles.EmphasizedLiteral)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-samp", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "samp", - "is_markdown": true, - "description": [ - "A piece of literal text, such as code. Within the contents, you can use", - "curly braces to indicate a \"variable\" part, as in [file](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-file). For", - "example, in `:samp:`print 1+{variable}``, the part `variable` would be", - "emphasized.", - "", - "If you don't need the \"variable part\" indication, use the standard", - "```code``` instead." - ], - "options": {} - }, - "term(sphinx.roles.XRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-term", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "term", - "is_markdown": true, - "description": [ - "Reference to a term in a glossary. A glossary is created using the", - "`glossary` directive containing a definition list with terms and", - "definitions. It does not have to be in the same file as the `term` markup,", - "for example the Python docs have one global glossary in the `glossary.rst`", - "file.", - "", - "If you use a term that's not explained in a glossary, you'll get a warning", - "during build." - ], - "options": {} - }, - "token(sphinx.domains.std.TokenXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-token", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "token", - "is_markdown": true, - "description": [ - "The name of a grammar token (used to create links between", - "[productionlist](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-productionlist) directives)." - ], - "options": {} - }, - "c:data(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:data", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "c:enum(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-enum", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:enum", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "c:enumerator(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-enumerator", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:enumerator", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "c:expr(sphinx.domains.c.CExprRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-expr", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:expr", - "is_markdown": true, - "description": [ - "Insert a C expression or type either as inline code (`cpp:expr`)", - "or inline text (`cpp:texpr`). For example:", - "", - "```", - ".. c:var:: int a = 42", - "", - ".. c:function:: int f(int i)", - "", - "An expression: :c:expr:`a * f(a)` (or as text: :c:texpr:`a * f(a)`).", - "", - "A type: :c:expr:`const Data*`", - "(or as text :c:texpr:`const Data*`).", - "```", - "", - "will be rendered as follows:", - "", - "", - "", - "", - "", - "An expression: `a * f(a)` (or as text: `a * f(a)`).", - "", - "A type: `const Data*`", - "(or as text `const Data*`)." - ], - "options": {} - }, - "c:func(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-func", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:func", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "c:macro(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-macro", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:macro", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "c:member(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:member", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "c:struct(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-struct", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:struct", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "c:texpr(sphinx.domains.c.CExprRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-texpr", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:texpr", - "is_markdown": true, - "description": [ - "Insert a C expression or type either as inline code (`cpp:expr`)", - "or inline text (`cpp:texpr`). For example:", - "", - "```", - ".. c:var:: int a = 42", - "", - ".. c:function:: int f(int i)", - "", - "An expression: :c:expr:`a * f(a)` (or as text: :c:texpr:`a * f(a)`).", - "", - "A type: :c:expr:`const Data*`", - "(or as text :c:texpr:`const Data*`).", - "```", - "", - "will be rendered as follows:", - "", - "", - "", - "", - "", - "An expression: `a * f(a)` (or as text: `a * f(a)`).", - "", - "A type: `const Data*`", - "(or as text `const Data*`)." - ], - "options": {} - }, - "c:type(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-type", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:type", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "c:union(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-union", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:union", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "c:var(sphinx.domains.c.CXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "c:var", - "is_markdown": true, - "description": [ - "Reference a C declaration, as defined above.", - "Note that [c:member](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-member), [c:data](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-data), and", - "[c:var](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-c-var) are equivalent." - ], - "options": {} - }, - "cpp:any(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-any", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:any", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "cpp:class(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-class", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:class", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "cpp:concept(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-concept", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:concept", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "cpp:enum(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-enum", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:enum", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "cpp:enumerator(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-enumerator", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:enumerator", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "cpp:expr(sphinx.domains.cpp.CPPExprRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-expr", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:expr", - "is_markdown": true, - "description": [ - "Insert a C++ expression or type either as inline code (`cpp:expr`)", - "or inline text (`cpp:texpr`). For example:", - "", - "```", - ".. cpp:var:: int a = 42", - "", - ".. cpp:function:: int f(int i)", - "", - "An expression: :cpp:expr:`a * f(a)` (or as text: :cpp:texpr:`a * f(a)`).", - "", - "A type: :cpp:expr:`const MySortedContainer<int>&`", - "(or as text :cpp:texpr:`const MySortedContainer<int>&`).", - "```", - "", - "will be rendered as follows:", - "", - "", - "", - "", - "", - "An expression: `a * f(a)` (or as text: `a * f(a)`).", - "", - "A type: `const MySortedContainer<int>&`", - "(or as text `const MySortedContainer<int>&`)." - ], - "options": {} - }, - "cpp:func(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-func", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:func", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "cpp:member(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-member", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:member", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "cpp:struct(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-struct", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:struct", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "cpp:texpr(sphinx.domains.cpp.CPPExprRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-texpr", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:texpr", - "is_markdown": true, - "description": [ - "Insert a C++ expression or type either as inline code (`cpp:expr`)", - "or inline text (`cpp:texpr`). For example:", - "", - "```", - ".. cpp:var:: int a = 42", - "", - ".. cpp:function:: int f(int i)", - "", - "An expression: :cpp:expr:`a * f(a)` (or as text: :cpp:texpr:`a * f(a)`).", - "", - "A type: :cpp:expr:`const MySortedContainer<int>&`", - "(or as text :cpp:texpr:`const MySortedContainer<int>&`).", - "```", - "", - "will be rendered as follows:", - "", - "", - "", - "", - "", - "An expression: `a * f(a)` (or as text: `a * f(a)`).", - "", - "A type: `const MySortedContainer<int>&`", - "(or as text `const MySortedContainer<int>&`)." - ], - "options": {} - }, - "cpp:type(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-type", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:type", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "cpp:var(sphinx.domains.cpp.CPPXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-cpp-var", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "cpp:var", - "is_markdown": true, - "description": [ - "Reference a C++ declaration by name (see below for details). The name must", - "be properly qualified relative to the position of the link." - ], - "options": {} - }, - "js:attr(sphinx.domains.javascript.JSXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-js-attr", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:attr", - "is_markdown": true, - "description": [ - "" - ], - "options": {} - }, - "js:class(sphinx.domains.javascript.JSXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-js-class", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:class", - "is_markdown": true, - "description": [ - "" - ], - "options": {} - }, - "js:data(sphinx.domains.javascript.JSXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-js-data", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:data", - "is_markdown": true, - "description": [ - "" - ], - "options": {} - }, - "js:func(sphinx.domains.javascript.JSXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-js-func", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:func", - "is_markdown": true, - "description": [ - "" - ], - "options": {} - }, - "js:meth(sphinx.domains.javascript.JSXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-js-meth", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:meth", - "is_markdown": true, - "description": [ - "" - ], - "options": {} - }, - "js:mod(sphinx.domains.javascript.JSXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-js-mod", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "js:mod", - "is_markdown": true, - "description": [ - "" - ], - "options": {} - }, - "math:numref(sphinx.domains.math.MathReferenceRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-math-numref", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "math:numref", - "is_markdown": true, - "description": [ - "Role for cross-referencing equations defined by [math](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-math) directive", - "via their label. Example:", - "", - "```", - ".. math:: e^{i\\pi} + 1 = 0", - " :label: euler", - "", - "Euler's identity, equation :math:numref:`euler`, was elected one of the", - "most beautiful mathematical formulas.", - "```" - ], - "options": {} - }, - "py:attr(sphinx.domains.python.PyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-attr", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:attr", - "is_markdown": true, - "description": [ - "Reference a data attribute of an object.", - "", - "The role is also able to refer to property." - ], - "options": {} - }, - "py:class(sphinx.domains.python.PyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-class", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:class", - "is_markdown": true, - "description": [ - "Reference a class; a dotted name may be used." - ], - "options": {} - }, - "py:const(sphinx.domains.python.PyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-const", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:const", - "is_markdown": true, - "description": [ - "Reference a \"defined\" constant. This may be a Python variable that is not", - "intended to be changed." - ], - "options": {} - }, - "py:data(sphinx.domains.python.PyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-data", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:data", - "is_markdown": true, - "description": [ - "Reference a module-level variable." - ], - "options": {} - }, - "py:exc(sphinx.domains.python.PyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-exc", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:exc", - "is_markdown": true, - "description": [ - "Reference an exception. A dotted name may be used." - ], - "options": {} - }, - "py:func(sphinx.domains.python.PyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-func", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:func", - "is_markdown": true, - "description": [ - "Reference a Python function; dotted names may be used. The role text needs", - "not include trailing parentheses to enhance readability; they will be added", - "automatically by Sphinx if the [add_function_parentheses](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-add_function_parentheses) config", - "value is `True` (the default)." - ], - "options": {} - }, - "py:meth(sphinx.domains.python.PyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-meth", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:meth", - "is_markdown": true, - "description": [ - "Reference a method of an object. The role text can include the type name", - "and the method name; if it occurs within the description of a type, the type", - "name can be omitted. A dotted name may be used." - ], - "options": {} - }, - "py:mod(sphinx.domains.python.PyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-mod", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:mod", - "is_markdown": true, - "description": [ - "Reference a module; a dotted name may be used. This should also be used for", - "package names." - ], - "options": {} - }, - "py:obj(sphinx.domains.python.PyXRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-py-obj", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "py:obj", - "is_markdown": true, - "description": [ - "Reference an object of unspecified type. Useful e.g. as the", - "[default_role](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-default_role)." - ], - "options": {} - }, - "rst:dir(sphinx.roles.XRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-rst-dir", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "rst:dir", - "is_markdown": true, - "description": [ - "" - ], - "options": {} - }, - "rst:role(sphinx.roles.XRefRole)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#role-rst-role", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "rst:role", - "is_markdown": true, - "description": [ - "" - ], - "options": {} - }, - "index(docutils.parsers.rst.roles.unimplemented_role)": { - "source": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#role-index", - "license": "https://github.com/sphinx-doc/sphinx/blob/4.x/LICENSE", - "name": "index", - "is_markdown": true, - "description": [ - "While the [index](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-index) directive is a block-level markup and links to the", - "beginning of the next paragraph, there is also a corresponding role that sets", - "the link target directly where it is used.", - "", - "The content of the role can be a simple phrase, which is then kept in the", - "text and used as an index entry. It can also be a combination of text and", - "index entry, styled like with explicit targets of cross-references. In that", - "case, the \"target\" part can be a full entry as described for the directive", - "above. For example:", - "", - "```", - "This is a normal reST :index:`paragraph` that contains several", - ":index:`index entries <pair: index; entry>`.", - "```" - ], - "options": {} - } -} diff --git a/lib/esbonio/esbonio/lsp/sphinx/roles.py b/lib/esbonio/esbonio/lsp/sphinx/roles.py deleted file mode 100644 index d376902ba..000000000 --- a/lib/esbonio/esbonio/lsp/sphinx/roles.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Extra support for roles added by sphinx.""" -import json -import os.path -from typing import List -from typing import Optional - -import pygls.uris as Uri -from lsprotocol.types import CompletionItem - -from esbonio.lsp.roles import Roles -from esbonio.lsp.rst import CompletionContext -from esbonio.lsp.sphinx import SphinxLanguageServer -from esbonio.lsp.util import resources -from esbonio.lsp.util.filepaths import complete_sphinx_filepaths -from esbonio.lsp.util.filepaths import path_to_completion_item - - -class Downloads: - def __init__(self, rst: SphinxLanguageServer): - self.rst = rst - self.logger = rst.logger.getChild(self.__class__.__name__) - - def complete_targets( - self, context: CompletionContext, name: str, domain: Optional[str] - ) -> List[CompletionItem]: - if domain or name != "download": - return [] - - if not self.rst.app: - return [] - - srcdir = self.rst.app.srcdir - partial = context.match.group("label") - base = os.path.dirname(Uri.to_fs_path(context.doc.uri)) - items = complete_sphinx_filepaths(srcdir, base, partial) # type: ignore[arg-type] - - return [path_to_completion_item(context, p) for p in items] - - -def esbonio_setup(rst: SphinxLanguageServer, roles: Roles): - sphinx_docs = resources.read_string("esbonio.lsp.sphinx", "roles.json") - roles.add_documentation(json.loads(sphinx_docs)) - - roles.add_target_completion_provider(Downloads(rst)) diff --git a/lib/esbonio/esbonio/lsp/symbols.py b/lib/esbonio/esbonio/lsp/symbols.py deleted file mode 100644 index bdfb7b21c..000000000 --- a/lib/esbonio/esbonio/lsp/symbols.py +++ /dev/null @@ -1,131 +0,0 @@ -from typing import Optional - -from docutils import nodes -from docutils.nodes import NodeVisitor -from lsprotocol.types import DocumentSymbol -from lsprotocol.types import Position -from lsprotocol.types import Range -from lsprotocol.types import SymbolKind - - -class SymbolVisitor(NodeVisitor): - """A visitor used to build the hierarchy we return from a - ``textDocument/documentSymbol`` request. - - """ - - def __init__(self, rst, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.logger = rst.logger - self.symbols = [] - self.symbol_stack = [] - - @property - def current_symbol(self) -> Optional[DocumentSymbol]: - if len(self.symbol_stack) == 0: - return None - - return self.symbol_stack[-1] - - def push_symbol(self): - symbol = DocumentSymbol( - name="", - kind=SymbolKind.String, - range=Range( - start=Position(line=1, character=0), - end=Position(line=1, character=10), - ), - selection_range=Range( - start=Position(line=1, character=0), - end=Position(line=1, character=10), - ), - children=[], - ) - current_symbol = self.current_symbol - - if not current_symbol: - self.symbols.append(symbol) - else: - if current_symbol.children is None: - current_symbol.children = [symbol] - else: - current_symbol.children.append(symbol) - - self.symbol_stack.append(symbol) - return symbol - - def pop_symbol(self): - self.symbol_stack.pop() - - def visit_section(self, node: nodes.Node) -> None: - self.push_symbol() - - def depart_section(self, node: nodes.Node) -> None: - self.pop_symbol() - - def visit_title(self, node: nodes.Node) -> None: - symbol = self.current_symbol - has_parent = True - - if symbol is None: - has_parent = False - symbol = self.push_symbol() - - name = node.astext() - line = (node.line or 1) - 1 - - symbol.name = name - symbol.range.start.line = line - symbol.range.end.line = line - symbol.range.end.character = len(name) - 1 - symbol.selection_range.start.line = line - symbol.selection_range.end.line = line - symbol.selection_range.end.character = len(name) - 1 - - if not has_parent: - self.pop_symbol() - - def depart_title(self, node: nodes.Node) -> None: - pass - - def visit_a_directive(self, node: nodes.Element): - symbol = self.push_symbol() - - name = node["text"] # type: ignore - line = (node.line or 1) - 1 - - symbol.name = name - symbol.kind = SymbolKind.Class - symbol.range.start.line = line - symbol.range.end.line = line - symbol.range.end.character = len(name) - 1 - symbol.selection_range.start.line = line - symbol.selection_range.end.line = line - symbol.selection_range.end.character = len(name) - 1 - - def depart_a_directive(self, node: nodes.Node): - self.pop_symbol() - - # TODO: Enable symbols for roles - # However the reported line numbers can be inaccurate... - def visit_a_role(self, node: nodes.Node) -> None: - ... - - def depart_a_role(self, node: nodes.Node) -> None: - ... - - # TODO: Enable symbols for definition list items - # However the reported line numbers appear to be inaccurate... - - def visit_Text(self, node: nodes.Node) -> None: - pass - - def depart_Text(self, node: nodes.Node) -> None: - pass - - def unknown_visit(self, node: nodes.Node) -> None: - pass - - def unknown_departure(self, node: nodes.Node) -> None: - pass diff --git a/lib/esbonio/esbonio/lsp/testing.py b/lib/esbonio/esbonio/lsp/testing.py deleted file mode 100644 index a9acb9010..000000000 --- a/lib/esbonio/esbonio/lsp/testing.py +++ /dev/null @@ -1,428 +0,0 @@ -"""Utility functions to help with testing Language Server features.""" -import logging -import pathlib -import re -from typing import List -from typing import Optional -from typing import Union - -import pygls.uris as Uri -from lsprotocol.types import ClientCapabilities -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionList -from lsprotocol.types import CompletionParams -from lsprotocol.types import DidChangeTextDocumentParams -from lsprotocol.types import DidCloseTextDocumentParams -from lsprotocol.types import DidOpenTextDocumentParams -from lsprotocol.types import Hover -from lsprotocol.types import HoverParams -from lsprotocol.types import Position -from lsprotocol.types import Range -from lsprotocol.types import TextDocumentContentChangeEvent_Type1 -from lsprotocol.types import TextDocumentIdentifier -from lsprotocol.types import TextDocumentItem -from lsprotocol.types import VersionedTextDocumentIdentifier -from pygls.workspace import Document -from pytest_lsp import LanguageClient -from pytest_lsp import make_test_lsp_client -from sphinx import __version__ as __sphinx_version__ - -from esbonio.lsp import CompletionContext -from esbonio.lsp.rst.config import ServerCompletionConfig - -logger = logging.getLogger(__name__) - - -def _noop(*args, **kwargs): - ... - - -def make_esbonio_client(*args, **kwargs) -> LanguageClient: - """Construct a pytest-lsp client that is aware of esbonio specific messages""" - client = make_test_lsp_client(*args, **kwargs) - client.feature("esbonio/buildStart")(_noop) - client.feature("esbonio/buildComplete")(_noop) - - return client - - -def sphinx_version( - eq: Optional[int] = None, - lt: Optional[int] = None, - lte: Optional[int] = None, - gt: Optional[int] = None, - gte: Optional[int] = None, -) -> bool: - """Helper function for determining which version of Sphinx we are - testing with. - - .. note:: - - Currently this function only considers the major version number. - - Parameters - ---------- - eq - When set, this function returns ``True`` if Sphinx's version is exactly - what's given. - - gt - When set, this function returns ``True`` if Sphinx's version is strictly - greater than what's given - - gte - When set, this function returns ``True`` if Sphinx's version is greater than - or equal to what's given - - lt - When set, this function returns ``True`` if Sphinx's version is strictly - less than what's given - - lte - When set, this function returns ``True`` if Sphinx's version is less than - or equal to what's given - - """ - - major, _, _ = (int(v) for v in __sphinx_version__.split(".")) - - if eq is not None: - return major == eq - - if gt is not None: - return major > gt - - if gte is not None: - return major >= gte - - if lt is not None: - return major < lt - - if lte is not None: - return major <= lte - - return False - - -def range_from_str(spec: str) -> Range: - """Create a range from the given string ``a:b-x:y``""" - start, end = spec.split("-") - sl, sc = start.split(":") - el, ec = end.split(":") - - return Range( - start=Position(line=int(sl), character=int(sc)), - end=Position(line=int(el), character=int(ec)), - ) - - -def make_completion_context( - pattern: re.Pattern, - text: str, - *, - character: int = -1, - prefer_insert: bool = False, -) -> CompletionContext: - """Helper for making test completion context instances. - - Parameters - ---------- - pattern - The regular expression pattern that corresponds to the completion request. - - text - The text that "triggered" the completion request - - character - The character column at which the request is being made. - If ``-1`` (the default), it will be assumed that the request is being made at - the end of ``text``. - - prefer_insert - Flag to indicate if the ``preferred_insert_behavior`` option should be set to - ``insert`` - """ - - match = pattern.match(text) - if not match: - raise ValueError(f"'{text}' is not valid in this completion context") - - line = 0 - character = len(text) if character == -1 else character - - return CompletionContext( - doc=Document(uri="file:///test.txt"), - location="rst", - match=match, - position=Position(line=line, character=character), - config=ServerCompletionConfig( - preferred_insert_behavior="insert" if prefer_insert else "replace" - ), - capabilities=ClientCapabilities(), - ) - - -def directive_argument_patterns(name: str, partial: str = "") -> List[str]: - """Return a number of example directive argument patterns. - - These correspond to test cases where directive argument suggestions should be - generated. - - Parameters - ---------- - name: - The name of the directive to generate suggestions for. - partial: - The partial argument that the user has already entered. - """ - return [s.format(name, partial) for s in [".. {}:: {}", " .. {}:: {}"]] - - -def role_patterns(partial: str = "") -> List[str]: - """Return a number of example role patterns. - - These correspond to when role suggestions should be generated. - - Parameters - ---------- - partial: - The partial role name that the user has already entered - """ - return [ - s.format(partial) - for s in [ - "{}", - "({}", - "- {}", - " {}", - " ({}", - " - {}", - "some text {}", - "some text ({}", - " some text {}", - " some text ({}", - ] - ] - - -def role_target_patterns( - name: str, partial: str = "", include_modifiers: bool = True -) -> List[str]: - """Return a number of example role target patterns. - - These correspond to test cases where role target suggestions should be generated. - - Parameters - ---------- - name: - The name of the role to generate suggestions for. - partial: - The partial target that the user as already entered. - include_modifiers: - A flag to indicate if additional modifiers like ``!`` and ``~`` should be - included in the generated patterns. - """ - - patterns = [ - ":{}:`{}", - "(:{}:`{}", - "- :{}:`{}", - ":{}:`More Info <{}", - "(:{}:`More Info <{}", - " :{}:`{}", - " (:{}:`{}", - " - :{}:`{}", - " :{}:`Some Label <{}", - " (:{}:`Some Label <{}", - ] - - test_cases = [p.format(name, partial) for p in patterns] - - if include_modifiers: - test_cases += [p.format(name, "!" + partial) for p in patterns] - test_cases += [p.format(name, "~" + partial) for p in patterns] - - return test_cases - - -def intersphinx_target_patterns(name: str, project: str) -> List[str]: - """Return a number of example intersphinx target patterns. - - These correspond to cases where target completions may be generated - - Parameters - ---------- - name: str - The name of the role to generate examples for - project: str - The name of the project to generate examples for - """ - return [ - s.format(name, project) - for s in [ - ":{}:`{}:", - "(:{}:`{}:", - ":{}:`More Info <{}:", - "(:{}:`More Info <{}:", - " :{}:`{}:", - " (:{}:`{}:", - " :{}:`Some Label <{}:", - " (:{}:`Some Label <{}:", - ] - ] - - -async def completion_request( - client: LanguageClient, test_uri: str, text: str, character: Optional[int] = None -) -> Union[CompletionList, List[CompletionItem], None]: - """Make a completion request to a language server. - - Intended for use within test cases, this function simulates the opening of a - document, inserting some text, triggering a completion request and closing it - again. - - The file referenced by ``test_uri`` does not have to exist. - - The text to be inserted is specified through the ``text`` parameter. By default - it's assumed that the ``text`` parameter consists of a single line of text, in fact - this function will error if that is not the case. - - If your request requires additional context (such as directive option completions) - it can be included but it must be delimited with a ``\\f`` character. For example, - to represent the following scenario:: - - .. image:: filename.png - :align: center - : - ^ - - where ``^`` represents the position from which we trigger the completion request. - We would set ``text`` to the following - ``.. image:: filename.png\\n :align: center\\n\\f :`` - - Parameters - ---------- - test: - The client used to make the request. - test_uri: - The uri the completion request should be made within. - text - The text that provides the context for the completion request. - character: - The character index at which to make the completion request from. - If ``None``, it will default to the end of the inserted text. - """ - - if "\f" in text: - contents, text = text.split("\f") - else: - contents = "" - - logger.debug("Context text: '%s'", contents) - logger.debug("Insertion text: '%s'", text) - assert "\n" not in text, "Insertion text cannot contain newlines" - - ext = pathlib.Path(Uri.to_fs_path(test_uri)).suffix - lang_id = "python" if ext == ".py" else "rst" - - client.text_document_did_open( - DidOpenTextDocumentParams( - text_document=TextDocumentItem( - uri=test_uri, language_id=lang_id, version=1, text=contents - ) - ) - ) - - lines = contents.split("\n") - line = len(lines) - 1 - insertion_point = len(lines[-1]) - - new_lines = text.split("\n") - num_new_lines = len(new_lines) - 1 - num_new_chars = len(new_lines[-1]) - - if num_new_lines > 0: - end_char = num_new_chars - else: - end_char = insertion_point + num_new_chars - - client.text_document_did_change( - DidChangeTextDocumentParams( - text_document=VersionedTextDocumentIdentifier(uri=test_uri, version=2), - content_changes=[ - TextDocumentContentChangeEvent_Type1( - text=text, - range=Range( - start=Position(line=line, character=insertion_point), - end=Position(line=line + num_new_lines, character=end_char), - ), - ) - ], - ) - ) - - character = character or insertion_point + len(text) - response = await client.text_document_completion_async( - CompletionParams( - text_document=TextDocumentIdentifier(uri=test_uri), - position=Position(line=line, character=character), - ) - ) - - client.text_document_did_close( - DidCloseTextDocumentParams(text_document=TextDocumentIdentifier(uri=test_uri)) - ) - - return response - - -async def hover_request( - client: LanguageClient, test_uri: str, text: str, line: int, character: int -) -> Optional[Hover]: - """Make a hover request to a language server. - - Intended for use within test cases, this function simulates the opening of a - document containing some text, triggering a hover request and closing it again. - - The file referenced by ``test_uri`` does not have to exist. - - Parameters - ---------- - test - The client used to make the request. - - test_uri - The uri the completion request should be made within. - - text - The text that provides the context for the hover request. - - line - The line number to make the hover request from - - character - The column number to make the hover request from - """ - ext = pathlib.Path(Uri.to_fs_path(test_uri)).suffix - lang_id = "python" if ext == ".py" else "rst" - - client.text_document_did_open( - DidOpenTextDocumentParams( - text_document=TextDocumentItem( - uri=test_uri, language_id=lang_id, version=1, text=text - ) - ) - ) - - response = await client.text_document_hover_async( - HoverParams( - text_document=TextDocumentIdentifier(uri=test_uri), - position=Position(line=line, character=character), - ) - ) - - client.text_document_did_close( - DidCloseTextDocumentParams(text_document=TextDocumentIdentifier(uri=test_uri)) - ) - - return response diff --git a/lib/esbonio/esbonio/lsp/util/__init__.py b/lib/esbonio/esbonio/lsp/util/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/esbonio/esbonio/lsp/util/filepaths.py b/lib/esbonio/esbonio/lsp/util/filepaths.py deleted file mode 100644 index f464b1fea..000000000 --- a/lib/esbonio/esbonio/lsp/util/filepaths.py +++ /dev/null @@ -1,133 +0,0 @@ -import pathlib -from typing import Generator - -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionItemKind -from lsprotocol.types import Position -from lsprotocol.types import Range -from lsprotocol.types import TextEdit - -from esbonio.lsp.rst import CompletionContext - - -def path_to_completion_item( - context: CompletionContext, path: pathlib.Path -) -> CompletionItem: - """Create the ``CompletionItem`` for the given path. - - In the case where there are multiple filepath components, this function needs to - provide an appropriate ``TextEdit`` so that the most recent entry in the path can - be easily edited - without clobbering the existing path. - - Also bear in mind that this function must play nice with both role target and - directive argument completions. - """ - - new_text = f"{path.name}" - kind = CompletionItemKind.Folder if path.is_dir() else CompletionItemKind.File - - # If we can't find the '/' we may as well not bother with a `TextEdit` and let the - # `Roles` feature provide the default handling. - start = _find_start_char(context) - if start == -1: - insert_text = new_text - filter_text = None - text_edit = None - else: - start += 1 - _, end = context.match.span() - prefix = context.match.group(0)[start:] - - insert_text = None - filter_text = ( - f"{prefix}{new_text}" # Needed so VSCode will actually show the results. - ) - - text_edit = TextEdit( - range=Range( - start=Position(line=context.position.line, character=start), - end=Position(line=context.position.line, character=end), - ), - new_text=new_text, - ) - - return CompletionItem( - label=new_text, - kind=kind, - insert_text=insert_text, - filter_text=filter_text, - text_edit=text_edit, - ) - - -def _find_start_char(context: CompletionContext) -> int: - matched_text = context.match.group(0) - idx = matched_text.find("/") - - while True: - next_idx = matched_text.find("/", idx + 1) - if next_idx == -1: - break - - idx = next_idx - - return idx - - -def complete_filepaths(base: str, partial: str) -> Generator[pathlib.Path, None, None]: - """Generate filepath completion suggestions relative to the given base. - - This function is for "docutils style" behaviour where the path is relative to the - current document. - - Parameters - ---------- - base - The directory containing the current document - - partial - The existing path entered so far. - """ - - candidate_dir = pathlib.Path(base) / pathlib.Path(partial) - if partial and not partial.endswith("/"): - candidate_dir = candidate_dir.parent - - return candidate_dir.glob("*") - - -def complete_sphinx_filepaths( - srcdir: str, base: str, partial: str -) -> Generator[pathlib.Path, None, None]: - """Generate filepath completion suggestions relative to the given base or ``srcdir``. - - This function is for "sphinx style" behaviour where the path is relative to the - current document *unless* ``partial`` starts with a ``/`` character. In this case - completions should be relative to the given ``srcdir``. - - Parameters - ---------- - srcdir - The ``srcdir`` of the project, used when ``partial`` starts with a ``/``. - - base - The directory containing the current document. - - partial - The existing path entered so far. - """ - - if partial and partial.startswith("/"): - candidate_dir = pathlib.Path(srcdir) - - # Be sure to take off the leading '/' character, otherwise the partial - # path will wipe out the srcdir when concatenated... - partial = partial[1:] - else: - candidate_dir = pathlib.Path(base) - - candidate_dir /= pathlib.Path(partial) - if partial and not partial.endswith("/"): - candidate_dir = candidate_dir.parent - - return candidate_dir.glob("*") diff --git a/lib/esbonio/esbonio/lsp/util/inspect.py b/lib/esbonio/esbonio/lsp/util/inspect.py deleted file mode 100644 index 2f5c5a424..000000000 --- a/lib/esbonio/esbonio/lsp/util/inspect.py +++ /dev/null @@ -1,42 +0,0 @@ -import inspect -import logging -import traceback -from typing import Optional - -import pygls.uris as Uri -from lsprotocol.types import Location -from lsprotocol.types import Position -from lsprotocol.types import Range - - -def get_object_location(obj: object, logger: logging.Logger) -> Optional[Location]: - """Given an object, attempt to find the location of its implementation. - - Parameters - ---------- - obj - The object to find the implementation of - - logger - A logger object - """ - - try: - file = inspect.getsourcefile(obj) # type: ignore - if file is None: - return None - - source, line = inspect.getsourcelines(obj) # type: ignore - return Location( - uri=Uri.from_fs_path(file), - range=Range( - start=Position(line=line - 1, character=0), - end=Position(line=line + len(source), character=0), - ), - ) - - except Exception: - logger.debug( - "Unable to get implementation location\n%s", traceback.format_exc() - ) - return None diff --git a/lib/esbonio/esbonio/lsp/util/patterns.py b/lib/esbonio/esbonio/lsp/util/patterns.py deleted file mode 100644 index bfaf36fff..000000000 --- a/lib/esbonio/esbonio/lsp/util/patterns.py +++ /dev/null @@ -1,161 +0,0 @@ -import re - -DIRECTIVE: "re.Pattern" = re.compile( - r""" - (\s*) # directives can be indented - (?P<directive> - \.\. # directives start with a comment - [ ]? # followed by a space - (?P<substitution>\| # this could be a substitution definition - (?P<substitution_text>[^|]+)? - \|?)? - [ ]? - ((?P<domain>[\w]+):(?!:))? # directives may include a domain - (?P<name>([\w-]|:(?!:))+)? # directives have a name - (::)? # directives end with '::' - ) - ([\s]+(?P<argument>.*?)\s*$)? # directives may take an argument - """, - re.VERBOSE, -) -"""A regular expression to detect and parse partial and complete directives. - -This does **not** include any options or content that may be included underneath -the initial declaration. A number of named capture groups are available. - -``name`` - The name of the directive, not including the domain prefix. - -``domain`` - The domain prefix - -``directive`` - Everything that makes up a directive, from the initial ``..`` up to and including the - ``::`` characters. - -``argument`` - All argument text. - -``substitution`` - If the directive is part of a substitution definition, this group will contain - -**Example** - -Here is an example with a "standard" directive - -.. include:: ../../../lib/esbonio/tests/doctests/example_directive_pattern.txt - -And here is an example with a substitution definition - -.. include:: ../../../lib/esbonio/tests/doctests/example_directive_substitution_pattern.txt - -""" - - -DIRECTIVE_OPTION: "re.Pattern" = re.compile( - r""" - (?P<indent>\s+) # directive options must be indented - (?P<option> - : # options start with a ':' - (?P<name>[\w-]+)? # options have a name - :? # options end with a ':' - ) - (\s* - (?P<value>.*) # options can have a value - )? - """, - re.VERBOSE, -) -"""A regular expression used to detect and parse partial and complete directive options. - -A number of named capture groups are available - -``name`` - The name of the option - -``option`` - The name of the option including the surrounding ``:`` characters. - -``indent`` - The whitespace characters making preceeding the initial ``:`` character - -``value`` - The value passed to the option - -**Example** - -.. include:: ../../../lib/esbonio/tests/doctests/example_directive_option_pattern.txt - -""" - - -ROLE = re.compile( - r""" - ([^\w:]|^\s*) # roles cannot be preceeded by letter chars - (?P<role> - : # roles begin with a ':' character - (?!:) # the next character cannot be a ':' - ((?P<domain>[\w]+):(?=\w))? # roles may include a domain (that must be followed by a word character) - ((?P<name>[\w-]+):?)? # roles have a name - ) - (?P<target> - ` # targets begin with a '`' character - ((?P<alias>[^<`>]*?)<)? # targets may specify an alias - (?P<modifier>[!~])? # targets may have a modifier - (?P<label>[^<`>]*)? # targets contain a label - >? # labels end with a '>' when there's an alias - `? # targets end with a '`' character - )? - """, - re.VERBOSE, -) -"""A regular expression to detect and parse parial and complete roles. - -I'm not sure if there are offical names for the components of a role, but the -language server breaks a role down into a number of parts:: - - vvvvvv label - v modifier(optional) - vvvvvvvv target - :c:function:`!malloc` - ^^^^^^^^^^^^ role - ^^^^^^^^ name - ^ domain (optional) - -The language server sometimes refers to the above as a "plain" role, in that the -role's target contains just the label of the object it is linking to. However it's -also possible to define "aliased" roles, where the link text in the final document -is overriden, for example:: - - vvvvvvvvvvvvvvvvvvvvvvvv alias - vvvvvv label - v modifier (optional) - vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv target - :c:function:`used to allocate memory <~malloc>` - ^^^^^^^^^^^^ role - ^^^^^^^^ name - ^ domain (optional) - -See :func:`tests.test_roles.test_role_regex` for a list of example strings this pattern -is expected to match. -""" - - -DEFAULT_ROLE = re.compile( - r""" - (?<![:`]) - (?P<target> - ` # targets begin with a '`' character - ((?P<alias>[^<`>]*?)<)? # targets may specify an alias - (?P<modifier>[!~])? # targets may have a modifier - (?P<label>[^<`>]*)? # targets contain a label - >? # labels end with a '>' when there's an alias - `? # targets end with a '`' character - ) - """, - re.VERBOSE, -) -"""A regular expression to detect and parse parial and complete "default" roles. - -A "default" role is the target part of a normal role - but without the ``:name:`` part. -""" diff --git a/lib/esbonio/esbonio/lsp/util/resources.py b/lib/esbonio/esbonio/lsp/util/resources.py deleted file mode 100644 index b25de47bc..000000000 --- a/lib/esbonio/esbonio/lsp/util/resources.py +++ /dev/null @@ -1,30 +0,0 @@ -import importlib.resources - - -def read_string(package: str, filename: str) -> str: - """Light wrapper around ``importlib.resources`` that should work across Python - versions. - - Parameters - ---------- - package - The module/package to read from - - filename - The file within ``package`` to read - - Returns - ------- - str - The contents of the specified text file. - """ - - # `files` only available in Python 3.9+ - if hasattr(importlib.resources, "files"): - with importlib.resources.files(package).joinpath(filename).open("r") as f: - return f.read() - - # `open_text` deprecated in Python 3.11, so let's only rely on it when we - # have to. - with importlib.resources.open_text(package, filename) as f: - return f.read() diff --git a/lib/esbonio/esbonio/server/__init__.py b/lib/esbonio/esbonio/server/__init__.py index c5fe71ed4..c06bbc762 100644 --- a/lib/esbonio/esbonio/server/__init__.py +++ b/lib/esbonio/esbonio/server/__init__.py @@ -1,20 +1,24 @@ from esbonio.sphinx_agent.types import Uri -from ._log import LOG_NAMESPACE -from ._log import MemoryHandler +from ._configuration import ConfigChangeEvent +from .events import EventSource from .feature import CompletionConfig from .feature import CompletionContext from .feature import LanguageFeature from .server import EsbonioLanguageServer from .server import EsbonioWorkspace +from .server import __version__ +from .setup import create_language_server -__all__ = [ - "LOG_NAMESPACE", +__all__ = ( + "__version__", + "ConfigChangeEvent", "CompletionConfig", "CompletionContext", "EsbonioLanguageServer", "EsbonioWorkspace", + "EventSource", "LanguageFeature", - "MemoryHandler", "Uri", -] + "create_language_server", +) diff --git a/lib/esbonio/esbonio/server/_configuration.py b/lib/esbonio/esbonio/server/_configuration.py index 9b9e1bd64..fb6ed333a 100644 --- a/lib/esbonio/esbonio/server/_configuration.py +++ b/lib/esbonio/esbonio/server/_configuration.py @@ -1,20 +1,14 @@ from __future__ import annotations +import asyncio import inspect import json import pathlib +import traceback import typing -from typing import Any -from typing import Awaitable -from typing import Callable -from typing import Dict +from functools import partial from typing import Generic -from typing import List -from typing import Optional -from typing import Set -from typing import Type from typing import TypeVar -from typing import Union import attrs from lsprotocol import types @@ -22,16 +16,31 @@ from . import Uri +T = TypeVar("T") + if typing.TYPE_CHECKING: + from typing import Any + from typing import Awaitable + from typing import Callable + from typing import Dict + from typing import List + from typing import Optional + from typing import Set + from typing import Type + from typing import Union + from .server import EsbonioLanguageServer + ConfigurationCallback = Callable[ + ["ConfigChangeEvent"], Union[Awaitable[None], None] + ] + + try: import tomllib as toml except ImportError: import tomli as toml # type: ignore[no-redef] -T = TypeVar("T") - @attrs.define(frozen=True) class Subscription(Generic[T]): @@ -43,7 +52,7 @@ class Subscription(Generic[T]): spec: Type[T] """The subscription's class definition.""" - callback: Callable[[T], Union[Awaitable[None], None]] + callback: ConfigurationCallback """The subscription's callback.""" workspace_scope: str @@ -53,6 +62,20 @@ class Subscription(Generic[T]): """The corresponding file scope for the subscription.""" +@attrs.define +class ConfigChangeEvent(Generic[T]): + """Is sent to subscribers when a configuration change occurs.""" + + scope: str + """The scope at which this configuration change occured.""" + + value: T + """The latest configuration value.""" + + previous: Optional[T] = None + """The previous configuration value, (if any).""" + + class Configuration: """Manages the configuration values for the server. @@ -91,6 +114,9 @@ def __init__(self, server: EsbonioLanguageServer): self._subscriptions: Dict[Subscription, Any] = {} """Subscriptions and their last known value""" + self._tasks: Set[asyncio.Task] = set() + """Holds tasks that are currently executing an async config handler.""" + @property def initialization_options(self): return self._initialization_options @@ -121,11 +147,11 @@ def supports_workspace_config(self): self.server.client_capabilities, "workspace.configuration", False ) - async def subscribe( + def subscribe( self, section: str, spec: Type[T], - callback: Callable[[T], Union[Awaitable[None], None]], + callback: ConfigurationCallback, scope: Optional[Uri] = None, ): """Subscribe to updates to the given configuration section. @@ -154,22 +180,12 @@ async def subscribe( self.logger.debug("Ignoring duplicate subscription: %s", subscription) return - # Wait until the server is ready before fetching the initial configuration - await self.server.ready - - result = self.get(section, spec, scope) - self._subscriptions[subscription] = result + self._subscriptions[subscription] = None - try: - ret = callback(result) - if inspect.isawaitable(ret): - await ret - except Exception: - self.logger.error( - "Error in configuration callback: %s", callback, exc_info=True - ) + # Once the server is ready, update all the subscriptions + self.server.ready.add_done_callback(self._notify_subscriptions) - async def _notify_subscriptions(self): + def _notify_subscriptions(self, *args): """Notify subscriptions about configuration changes, if necessary.""" for subscription, previous_value in self._subscriptions.items(): @@ -186,12 +202,22 @@ async def _notify_subscriptions(self): if previous_value == value: continue + self._subscriptions[subscription] = value + change_event = ConfigChangeEvent( + scope=max( + [subscription.file_scope, subscription.workspace_scope], key=len + ), + value=value, + previous=previous_value, + ) + self.logger.info("%s", change_event) + try: - ret = subscription.callback(value) - if inspect.isawaitable(ret): - await ret + ret = subscription.callback(change_event) + if inspect.iscoroutine(ret): + task = asyncio.create_task(ret) + task.add_done_callback(partial(self._finish_task, subscription)) - self._subscriptions[subscription] = value except Exception: self.logger.error( "Error in configuration callback: %s", @@ -199,6 +225,17 @@ async def _notify_subscriptions(self): exc_info=True, ) + def _finish_task(self, subscription: Subscription, task: asyncio.Task[None]): + """Cleanup a finished task.""" + self._tasks.discard(task) + + if (exc := task.exception()) is not None: + self.logger.error( + "Error in async configuration handler '%s'\n%s", + subscription.callback, + "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)), + ) + def get(self, section: str, spec: Type[T], scope: Optional[Uri] = None) -> T: """Get the requested configuration section. @@ -223,6 +260,25 @@ def get(self, section: str, spec: Type[T], scope: Optional[Uri] = None) -> T: return self._get_config(section, spec, workspace_scope, file_scope) + def scope_for(self, uri: Uri) -> str: + """Return the configuration scope that corresponds to the given uri. + + Parameters + ---------- + uri + The uri to return the scope for + + Returns + ------- + str + The scope corresponding with the given uri + """ + + file_scope = self._uri_to_file_scope(uri) + workspace_scope = self._uri_to_workspace_scope(uri) + + return max([file_scope, workspace_scope], key=len) + def _get_config( self, section: str, spec: Type[T], workspace_scope: str, file_scope: str ) -> T: @@ -293,9 +349,7 @@ def _discover_config_files(self) -> List[pathlib.Path]: return paths - async def update_file_configuration( - self, paths: Optional[List[pathlib.Path]] = None - ): + def update_file_configuration(self, paths: Optional[List[pathlib.Path]] = None): """Update the internal cache of configuration coming from files. Parameters @@ -322,7 +376,7 @@ async def update_file_configuration( "Unable to read configuration file: '%s'", exc_info=True ) - await self._notify_subscriptions() + self._notify_subscriptions() async def update_workspace_configuration(self): """Update the internal cache of the client's workspace configuration.""" @@ -363,7 +417,7 @@ async def update_workspace_configuration(self): self._workspace_config[scope or ""] = result - await self._notify_subscriptions() + self._notify_subscriptions() def _uri_to_scope(known_scopes: List[str], uri: Optional[Uri]) -> str: diff --git a/lib/esbonio/esbonio/server/_log.py b/lib/esbonio/esbonio/server/_log.py deleted file mode 100644 index 18c402e90..000000000 --- a/lib/esbonio/esbonio/server/_log.py +++ /dev/null @@ -1,15 +0,0 @@ -import logging -from typing import List - -LOG_NAMESPACE = "esbonio" - - -class MemoryHandler(logging.Handler): - """A logging handler that caches messages in memory.""" - - def __init__(self): - super().__init__() - self.records: List[logging.LogRecord] = [] - - def emit(self, record: logging.LogRecord) -> None: - self.records.append(record) diff --git a/lib/esbonio/esbonio/server/cli.py b/lib/esbonio/esbonio/server/cli.py index 6bf85d21f..ff26bcfe9 100644 --- a/lib/esbonio/esbonio/server/cli.py +++ b/lib/esbonio/esbonio/server/cli.py @@ -2,13 +2,12 @@ import logging import sys import warnings +from logging.handlers import MemoryHandler from typing import Optional from typing import Sequence from pygls.protocol import default_converter -from ._log import LOG_NAMESPACE -from ._log import MemoryHandler from .server import EsbonioLanguageServer from .server import __version__ from .setup import create_language_server @@ -60,16 +59,13 @@ def build_parser() -> argparse.ArgumentParser: def main(argv: Optional[Sequence[str]] = None): - """Standard main function for each of the default language servers.""" - - # Put these here to avoid circular import issues. - cli = build_parser() args = cli.parse_args(argv) # Order matters! modules = [ "esbonio.server.features.log", + "esbonio.server.features.project_manager", "esbonio.server.features.sphinx_manager", "esbonio.server.features.preview_manager", "esbonio.server.features.directives", @@ -87,25 +83,21 @@ def main(argv: Optional[Sequence[str]] = None): # Ensure we can capture warnings. logging.captureWarnings(True) - warnlog = logging.getLogger("py.warnings") if not sys.warnoptions: warnings.simplefilter("default") # Enable capture of DeprecationWarnings # Setup a temporary logging handler that can cache messages until the language server # is ready to forward them onto the client. - logger = logging.getLogger(LOG_NAMESPACE) - logger.setLevel(logging.DEBUG) - - handler = MemoryHandler() - handler.setLevel(logging.DEBUG) - logger.addHandler(handler) - warnlog.addHandler(handler) + logging.basicConfig( + level=logging.DEBUG, + handlers=[MemoryHandler(999999, flushLevel=logging.CRITICAL)], + ) server = create_language_server( EsbonioLanguageServer, modules, - logger=logger, + logger=logging.getLogger("esbonio"), converter_factory=default_converter, ) diff --git a/lib/esbonio/esbonio/server/events.py b/lib/esbonio/esbonio/server/events.py new file mode 100644 index 000000000..d44e16334 --- /dev/null +++ b/lib/esbonio/esbonio/server/events.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import asyncio +import inspect +import logging +import traceback +import typing +from functools import partial + +if typing.TYPE_CHECKING: + from typing import Any + from typing import Dict + from typing import Optional + from typing import Set + + +class EventSource: + """Simple component for emitting events.""" + + # TODO: It might be nice to do some fancy typing here so that type checkers + # etc know which events are possible etc. + + def __init__(self, logger: Optional[logging.Logger] = None): + + self.logger = logger or logging.getLogger(__name__) + """The logging instance to use.""" + + self.handlers: Dict[str, set] = {} + """Collection of handlers for various events.""" + + self._tasks: Set[asyncio.Task] = set() + """Holds tasks that are currently executing an async event handler.""" + + def add_listener(self, event: str, handler): + """Add a listener for the given event name.""" + self.handlers.setdefault(event, set()).add(handler) + + def _finish_task(self, event: str, listener_name: str, task: asyncio.Task[Any]): + """Cleanup a finished task.""" + self._tasks.discard(task) + + if (exc := task.exception()) is not None: + self.logger.error( + "Error in '%s' async handler '%s'\n%s", + event, + listener_name, + "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)), + ) + + def trigger(self, event: str, *args, **kwargs): + """Trigger the event with the given name.""" + + for listener in self.handlers.get(event, set()): + listener_name = f"{listener}" + + try: + res = listener(*args, **kwargs) + + # Event listeners may be async + if inspect.iscoroutine(res): + task = asyncio.create_task(res) + task.add_done_callback( + partial(self._finish_task, event, listener_name) + ) + + self._tasks.add(task) + + except Exception: + self.logger.error( + "Error in '%s' handler '%s'", event, listener_name, exc_info=True + ) diff --git a/lib/esbonio/esbonio/server/feature.py b/lib/esbonio/esbonio/server/feature.py index c2fb0d5cd..e34712a5d 100644 --- a/lib/esbonio/esbonio/server/feature.py +++ b/lib/esbonio/esbonio/server/feature.py @@ -30,6 +30,11 @@ Coroutine[Any, Any, Optional[List[types.DocumentSymbol]]], ] + MaybeAsyncNone = Union[ + None, + Coroutine[Any, Any, None], + ] + WorkspaceSymbolResult = Union[ Optional[List[types.WorkspaceSymbol]], Coroutine[Any, Any, Optional[List[types.WorkspaceSymbol]]], @@ -51,22 +56,29 @@ def converter(self): def configuration(self): return self.server.configuration - def initialize(self, params: types.InitializeParams): + def initialize(self, params: types.InitializeParams) -> MaybeAsyncNone: """Called during ``initialize``.""" - def initialized(self, params: types.InitializedParams): + def initialized(self, params: types.InitializedParams) -> MaybeAsyncNone: """Called when the ``initialized`` notification is received.""" - def document_change(self, params: types.DidChangeTextDocumentParams): + def shutdown(self, params: None) -> MaybeAsyncNone: + """Called when the server is instructed to ``shutdown`` by the client.""" + + def document_change( + self, params: types.DidChangeTextDocumentParams + ) -> MaybeAsyncNone: """Called when a text document is changed.""" - def document_close(self, params: types.DidCloseTextDocumentParams): + def document_close( + self, params: types.DidCloseTextDocumentParams + ) -> MaybeAsyncNone: """Called when a text document is closed.""" - def document_open(self, params: types.DidOpenTextDocumentParams): + def document_open(self, params: types.DidOpenTextDocumentParams) -> MaybeAsyncNone: """Called when a text document is opened.""" - def document_save(self, params: types.DidSaveTextDocumentParams): + def document_save(self, params: types.DidSaveTextDocumentParams) -> MaybeAsyncNone: """Called when a text document is saved.""" completion_triggers: List["re.Pattern"] = [] diff --git a/lib/esbonio/esbonio/server/features/directives/__init__.py b/lib/esbonio/esbonio/server/features/directives/__init__.py index 48ed001b0..e00bb3582 100644 --- a/lib/esbonio/esbonio/server/features/directives/__init__.py +++ b/lib/esbonio/esbonio/server/features/directives/__init__.py @@ -63,17 +63,19 @@ def add_provider(self, provider: DirectiveProvider): completion_triggers = [RST_DIRECTIVE, MYST_DIRECTIVE] - async def initialized(self, params: types.InitializedParams): + def initialized(self, params: types.InitializedParams): """Called once the initial handshake between client and server has finished.""" - await self.configuration.subscribe( + self.configuration.subscribe( "esbonio.server.completion", server.CompletionConfig, self.update_configuration, ) - def update_configuration(self, config: server.CompletionConfig): + def update_configuration( + self, event: server.ConfigChangeEvent[server.CompletionConfig] + ): """Called when the user's configuration is updated.""" - self._insert_behavior = config.preferred_insert_behavior + self._insert_behavior = event.value.preferred_insert_behavior async def completion( self, context: server.CompletionContext diff --git a/lib/esbonio/esbonio/server/features/directives/completion.py b/lib/esbonio/esbonio/server/features/directives/completion.py index 4450449f1..dfc3abfcf 100644 --- a/lib/esbonio/esbonio/server/features/directives/completion.py +++ b/lib/esbonio/esbonio/server/features/directives/completion.py @@ -1,4 +1,5 @@ """Helper functions for completion support""" + from __future__ import annotations import re diff --git a/lib/esbonio/esbonio/server/features/log.py b/lib/esbonio/esbonio/server/features/log.py index fb4f984cc..394919e8b 100644 --- a/lib/esbonio/esbonio/server/features/log.py +++ b/lib/esbonio/esbonio/server/features/log.py @@ -1,241 +1,341 @@ import enum import json import logging -import pathlib -import re +import logging.config import textwrap -import traceback -from typing import List -from typing import Tuple +from logging.handlers import MemoryHandler +from typing import Any +from typing import Dict +from typing import Optional import attrs from lsprotocol import types -from esbonio.server import LOG_NAMESPACE -from esbonio.server import EsbonioLanguageServer -from esbonio.server import LanguageFeature -from esbonio.server import MemoryHandler -from esbonio.server import Uri +from esbonio import server -LOG_LEVELS = { - "debug": logging.DEBUG, - "error": logging.ERROR, - "info": logging.INFO, -} +LOG_LEVELS = {"CRITICAL", "FATAL", "ERROR", "WARN", "WARNING", "INFO", "DEBUG"} -# e.g. /.../filename.rst:54: (ERROR/3) Unexpected indentation. -# c:\...\filename.rst:54: (ERROR/3) Unexpected indentation. -DOCUTILS_ERROR = re.compile( - r""" - ^\s*(?P<filepath>.+?): - (?P<linum>\d+): - \s*\((?P<levelname>\w+)/(?P<levelnum>\d)\) - (?P<message>.*)$ - """, - re.VERBOSE, -) +class WindowLogMessageHandler(logging.Handler): + """A logging handler that will send log records to an LSP client as + ``window/logMessage`` notifications.""" -DOCUTILS_SEVERITY = { - 0: types.DiagnosticSeverity.Hint, - 1: types.DiagnosticSeverity.Information, - 2: types.DiagnosticSeverity.Warning, - 3: types.DiagnosticSeverity.Error, - 4: types.DiagnosticSeverity.Error, -} + def __init__( + self, + server: server.EsbonioLanguageServer, + ): + super().__init__() + self.server = server + def emit(self, record: logging.LogRecord) -> None: + """Sends the record to the client.""" -class LogFilter(logging.Filter): - """A log filter that accepts message from any of the listed logger names.""" + # To avoid infinite recursions, we can't process records coming from pygls. + if "pygls" in record.name: + return - def __init__(self, names): - self.names = names + log = self.format(record).strip() + self.server.show_message_log(log) - def filter(self, record): - return any(record.name == name for name in self.names) +@attrs.define +class LoggerConfiguration: + """Configuration options for a given logger.""" -class LspHandler(logging.Handler): - """A logging handler that will send log records to an LSP client.""" + level: Optional[str] = attrs.field(default=None) + """The logging level to use, if not set the default logging level will be used.""" - def __init__( - self, server: EsbonioLanguageServer, show_deprecation_warnings: bool = False - ): - super().__init__() - self.server = server - self.show_deprecation_warnings = show_deprecation_warnings + format: Optional[str] = attrs.field(default=None) + """The log format to use, if not set the default logging level will be used.""" - def get_warning_path(self, warning: str) -> Tuple[str, List[str]]: - """Determine the filepath that the warning was emitted from.""" + filepath: Optional[str] = attrs.field(default=None) + """If set log to a file""" - path, *parts = warning.split(":") + stderr: Optional[bool] = attrs.field(default=None) + """If True, log to stderr, if not set the default value will be used.""" - # On windows the rest of the path will be in the first element of parts. - if pathlib.Path(warning).drive: - path += f":{parts.pop(0)}" + window: Optional[bool] = attrs.field(default=None) + """If True, send message as a ``window/logMessage`` notification, if not set the + default value will be used""" - return path, parts - def handle_warning(self, record: logging.LogRecord): - """Publish warnings to the client as diagnostics.""" +class LoggingConfigBuilder: + """Helper class for converting the user's config into the logging config.""" - if not isinstance(record.args, tuple): - self.server.logger.debug( - "Unable to handle warning, expected tuple got: %s", record.args - ) - return + def __init__(self): + self.formatters = {} + self.handlers = {} + self.loggers = {} - # The way warnings are logged is different in Python 3.11+ - if len(record.args) == 0: - argument = record.msg - else: - argument = record.args[0] # type: ignore + def _get_formatter(self, format: str) -> str: + """Return the name of the formatter with the given format string. - if not isinstance(argument, str): - self.server.logger.debug( - "Unable to handle warning, expected string got: %s", argument - ) - return + If no such formatter exists, it will be created. + """ + for key, config in self.formatters.items(): + if config["format"] == format: + return key - warning, *_ = argument.split("\n") - path, (linenum, category, *msg) = self.get_warning_path(warning) + key = f"fmt{len(self.formatters) + 1:02d}" + self.formatters[key] = dict(format=format) + return key - category = category.strip() - message = ":".join(msg).strip() + def _get_file_handler(self, filepath: str, formatter: str) -> str: + """Return the name of the handler that will log to the given filepath using the + given formatter name. - try: - line = int(linenum) - except ValueError: - line = 1 - self.server.logger.debug( - "Unable to parse line number: '%s'\n%s", linenum, traceback.format_exc() - ) + If no such handler exists, it will be created. + """ + for key, config in self.handlers.items(): + if config.get("class", None) != "logging.FileHandler": + continue - tags = [] - if category == "DeprecationWarning": - tags.append(types.DiagnosticTag.Deprecated) - - diagnostic = types.Diagnostic( - range=types.Range( - start=types.Position(line=line - 1, character=0), - end=types.Position(line=line, character=0), - ), - message=message, - severity=types.DiagnosticSeverity.Warning, - tags=tags, - ) + if ( + config.get("formatter", None) == formatter + and config.get("filename", None) == filepath + ): + return key - self.server.add_diagnostics("esbonio", Uri.for_file(path), diagnostic) - self.server.sync_diagnostics() + key = f"file{len(self.handlers) + 1:02d}" + self.handlers[key] = { + "class": "logging.FileHandler", + "level": "DEBUG", # this way we can handle logs from loggers at any level + "formatter": formatter, + "filename": filepath, + } - def handle_diagnostic(self, record: logging.LogRecord): - """Look for any diagnostics to report in the log message.""" + return key - if (match := DOCUTILS_ERROR.match(record.msg)) is not None: - uri = Uri.for_file(match.group("filepath")) - line = int(match.group("linum")) - severity = int(match.group("levelnum")) + def _get_stderr_handler(self, formatter: str) -> str: + """Return the name of the handler that will log to stderr using the given + formatter name. - diagnostic = types.Diagnostic( - message=match.group("message").strip(), - severity=DOCUTILS_SEVERITY.get(severity), - range=types.Range( - start=types.Position(line=line - 1, character=0), - end=types.Position(line=line, character=0), - ), - ) - self.server.add_diagnostics("docutils", uri, diagnostic) + If no such handler exists, it will be created. + """ + for key, config in self.handlers.items(): + if config.get("class", None) != "logging.StreamHandler": + continue - def emit(self, record: logging.LogRecord) -> None: - """Sends the record to the client.""" + if config.get("formatter", None) == formatter: + return key - # To avoid infinite recursions, it's simpler to just ignore all log records - # coming from pygls... - if "pygls" in record.name: - return + key = f"stderr{len(self.handlers) + 1:02d}" + self.handlers[key] = { + "class": "logging.StreamHandler", + "level": "DEBUG", # this way we can handle logs from loggers at any level + "formatter": formatter, + "stream": "ext://sys.stderr", + } - if record.name == "py.warnings": - if not self.show_deprecation_warnings: - return + return key - self.handle_warning(record) - else: - self.handle_diagnostic(record) + def _get_window_handler( + self, server: server.EsbonioLanguageServer, formatter: str + ) -> str: + """Return the name of the handler that will send messages as + ``window/logMessage`` notificaitions, using the given formatter name. - log = self.format(record).strip() - self.server.show_message_log(log) + If no such formatter exists, it will be created. + """ + handler_class = f"{__name__}.WindowLogMessageHandler" + + for key, config in self.handlers.items(): + if config.get("()", None) != handler_class: + continue + + if config.get("formatter", None) == formatter: + return key + + key = f"window{len(self.handlers) + 1:02d}" + self.handlers[key] = { + "()": handler_class, + "level": "DEBUG", # this way we can handle logs from loggers at any level + "formatter": formatter, + "server": server, + } + + return key + + def add_logger( + self, + name: str, + level: str, + format: str, + filepath: Optional[str], + stderr: bool, + window: Optional[server.EsbonioLanguageServer], + ): + """Add a configuration for the given logger + + Parameters + ---------- + name + The logger name to add a configuration for + + level + The level at which to log messages + + format + The format string to apply to messages + + filepath + If set, record log messages in the given filepath + + stderr + If ``True``, print messages from this logger to stderr + + window + If set, send messages from this logger to the client as + ``window/logMessage`` notifications via the given server instance + """ + fmt = self._get_formatter(format) + handlers = [] + + if filepath: + handlers.append(self._get_file_handler(filepath, fmt)) + + if stderr: + handlers.append(self._get_stderr_handler(fmt)) + + if window: + handlers.append(self._get_window_handler(window, fmt)) + + if (level := level.upper()) not in LOG_LEVELS: + level = "DEBUG" + + self.loggers[name] = dict(level=level, propagate=False, handlers=handlers) + + def finish(self) -> Dict[str, Any]: + """Return the final configuration.""" + return dict( + version=1, + disable_existing_loggers=True, + formatters=self.formatters, + handlers=self.handlers, + loggers=self.loggers, + ) @attrs.define -class ServerLogConfig: +class LoggingConfig: """Configuration options for server logging.""" - log_filter: List[str] = attrs.field(factory=list) - """A list of logger names to restrict output to.""" + level: str = attrs.field(default="error") + """The default logging level.""" - log_level: str = attrs.field(default="error") - """The logging level of server messages to display.""" + format: str = attrs.field(default="[%(name)s] %(message)s") + """The log format string to use.""" - show_deprecation_warnings: bool = attrs.field(default=False) - """Developer flag to enable deprecation warnings.""" + filepath: Optional[str] = attrs.field(default=None) + """If set, log to a file by default""" + stderr: bool = attrs.field(default=True) + """If set, log to stderr by default""" -class LogManager(LanguageFeature): - """Manages the logging setup for the server.""" + window: bool = attrs.field(default=False) + """If set, send message as a ``window/logMessage`` notification""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + config: Dict[str, LoggerConfiguration] = attrs.field(factory=dict) + """Configuration of individual loggers""" - async def initialized(self, params: types.InitializedParams): - """Setup logging.""" - await self.server.configuration.subscribe( - "esbonio.server", ServerLogConfig, self.setup_logging - ) + show_deprecation_warnings: bool = attrs.field(default=False) + """Developer flag to enable deprecation warnings.""" - def setup_logging(self, config: ServerLogConfig): - """Setup logging to route log messages to the language client as - ``window/logMessage`` messages. + def to_logging_config(self, server: server.EsbonioLanguageServer) -> Dict[str, Any]: + """Convert the user's config into a config dict that can be passed to the + ``logging.config.dictConfig()`` function. Parameters ---------- server - The server to use to send messages - - config - The configuration to use + The language server instance (required for ``window/logMessages``) """ - level = LOG_LEVELS[config.log_level] + builder = LoggingConfigBuilder() + + # Ensure that there is at least an esbonio logger and a sphinx logger present in + # the config. + if "esbonio" not in self.config: + builder.add_logger( + "esbonio", + self.level, + self.format, + filepath=self.filepath, + stderr=self.stderr, + window=server if self.window else None, + ) - # warnlog = logging.getLogger("py.warnings") - logger = logging.getLogger(LOG_NAMESPACE) - logger.setLevel(level) + if "sphinx" not in self.config: + builder.add_logger( + "sphinx", + "info", + "%(message)s", + filepath=self.filepath, + stderr=self.stderr, + window=server if self.window else None, + ) - lsp_handler = LspHandler(self.server, config.show_deprecation_warnings) - lsp_handler.setLevel(level) + # Process any custom logger configuration + for name, logger_config in self.config.items(): + window = ( + logger_config.window + if logger_config.window is not None + else self.window + ) + builder.add_logger( + name, + logger_config.level or self.level, + logger_config.format or self.format, + filepath=( + logger_config.filepath + if logger_config.filepath is not None + else self.filepath + ), + stderr=( + logger_config.stderr + if logger_config.stderr is not None + else self.stderr + ), + window=server if window else None, + ) - if len(config.log_filter) > 0: - lsp_handler.addFilter(LogFilter(config.log_filter)) + return builder.finish() - formatter = logging.Formatter("[%(name)s] %(message)s") - lsp_handler.setFormatter(formatter) - # Look to see if there are any cached messages we should forward to the client. - for handler in logger.handlers: - # Remove any previous instances of the LspHandler - if isinstance(handler, LspHandler): - logger.removeHandler(handler) +class LogManager(server.LanguageFeature): + """Manages the logging setup for the server.""" + + def initialized(self, params: types.InitializedParams): + """Setup logging.""" + self.server.configuration.subscribe( + "esbonio.logging", LoggingConfig, self.setup_logging + ) + + def setup_logging(self, event: server.ConfigChangeEvent[LoggingConfig]): + """Setup logging according to the given config. + + Parameters + ---------- + event + The configuration change event + """ - # Forward any cached messages to the client - if isinstance(handler, MemoryHandler): - for record in handler.records: - if logger.isEnabledFor(record.levelno): - lsp_handler.emit(record) + records = [] + if event.previous is None: + # Messages received during initial startup are cached in a MemoryHandler + # instance, let's resuce them so they can be replayed against the new config. + for handler in logging.getLogger().handlers: + if isinstance(handler, MemoryHandler): + records = handler.buffer - logger.removeHandler(handler) + config = event.value.to_logging_config(self.server) + logging.config.dictConfig(config) - logger.addHandler(lsp_handler) - # warnlog.addHandler(lsp_handler) + # Replay any captured messages against the new config. + for record in records: + logger = logging.getLogger(record.name) + if logger.isEnabledFor(record.levelno): + logger.handle(record) def dump(obj) -> str: @@ -261,6 +361,6 @@ def default(o): return json.dumps(obj, default=default, indent=2) -def esbonio_setup(server: EsbonioLanguageServer): +def esbonio_setup(server: server.EsbonioLanguageServer): manager = LogManager(server) server.add_feature(manager) diff --git a/lib/esbonio/esbonio/server/features/preview_manager/__init__.py b/lib/esbonio/esbonio/server/features/preview_manager/__init__.py index acc08a0b0..bb4a2b8aa 100644 --- a/lib/esbonio/esbonio/server/features/preview_manager/__init__.py +++ b/lib/esbonio/esbonio/server/features/preview_manager/__init__.py @@ -1,5 +1,6 @@ import asyncio import logging +import sys from http.server import HTTPServer from http.server import SimpleHTTPRequestHandler from typing import Any @@ -12,6 +13,7 @@ from esbonio.server import EsbonioLanguageServer from esbonio.server import Uri from esbonio.server.feature import LanguageFeature +from esbonio.server.features.project_manager import ProjectManager from esbonio.server.features.sphinx_manager import SphinxClient from esbonio.server.features.sphinx_manager import SphinxManager @@ -79,11 +81,18 @@ class PreviewConfig: class PreviewManager(LanguageFeature): """Language feature for managing previews.""" - def __init__(self, server: EsbonioLanguageServer, sphinx: SphinxManager): + def __init__( + self, + server: EsbonioLanguageServer, + sphinx: SphinxManager, + projects: ProjectManager, + ): super().__init__(server) self.sphinx = sphinx self.sphinx.add_listener("build", self.on_build) + self.projects = projects + logger = server.logger.getChild("PreviewServer") self._request_handler_factory = RequestHandlerFactory(logger) self._http_server: Optional[HTTPServer] = None @@ -92,6 +101,20 @@ def __init__(self, server: EsbonioLanguageServer, sphinx: SphinxManager): self._ws_server: Optional[WebviewServer] = None self._ws_task: Optional[asyncio.Task] = None + def shutdown(self, params: None): + """Called when the client instructs the server to ``shutdown``.""" + args = {} + if sys.version_info.minor > 8: + args["msg"] = "Server is shutting down." + + if self._http_server: + self.logger.debug("Shutting down preview HTTP server") + self._http_server.shutdown() + + if self._ws_task: + self.logger.debug("Shutting down preview WebSocket server") + self._ws_task.cancel(**args) + @property def preview_active(self) -> bool: """Return true if the preview is active. @@ -184,18 +207,13 @@ async def preview_file(self, params): src_uri = Uri.parse(params["uri"]).resolve() self.logger.debug("Previewing file: '%s'", src_uri) - client = await self.sphinx.get_client(src_uri) - if client is None: + if (client := await self.sphinx.get_client(src_uri)) is None: return None - if client.builder not in {"html", "dirhtml"}: - self.logger.error( - "Previews for the '%s' builder are not currently supported", - client.builder, - ) + if (project := self.projects.get_project(src_uri)) is None: return None - if (build_path := await client.get_build_path(src_uri)) is None: + if (build_path := await project.get_build_path(src_uri)) is None: self.logger.debug( "Unable to preview file '%s', not included in build output.", src_uri ) @@ -222,8 +240,10 @@ async def preview_file(self, params): return {"uri": uri.as_string(encode=False)} -def esbonio_setup(server: EsbonioLanguageServer, sphinx: SphinxManager): - manager = PreviewManager(server, sphinx) +def esbonio_setup( + server: EsbonioLanguageServer, sphinx: SphinxManager, projects: ProjectManager +): + manager = PreviewManager(server, sphinx, projects) server.add_feature(manager) @server.feature("view/scroll") diff --git a/lib/esbonio/esbonio/server/features/preview_manager/webview.py b/lib/esbonio/esbonio/server/features/preview_manager/webview.py index 100360e0f..e68044e5d 100644 --- a/lib/esbonio/esbonio/server/features/preview_manager/webview.py +++ b/lib/esbonio/esbonio/server/features/preview_manager/webview.py @@ -1,5 +1,6 @@ """This module implements the websocket server used to communicate with preivew windows.""" + import asyncio import json import logging diff --git a/lib/esbonio/esbonio/server/features/project_manager/__init__.py b/lib/esbonio/esbonio/server/features/project_manager/__init__.py new file mode 100644 index 000000000..435af35d4 --- /dev/null +++ b/lib/esbonio/esbonio/server/features/project_manager/__init__.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from esbonio.server import EsbonioLanguageServer + +from .manager import ProjectManager +from .project import Project + +__all__ = [ + "Project", + "ProjectManager", +] + + +def esbonio_setup(server: EsbonioLanguageServer): + manager = ProjectManager(server) + server.add_feature(manager) diff --git a/lib/esbonio/esbonio/server/features/project_manager/manager.py b/lib/esbonio/esbonio/server/features/project_manager/manager.py new file mode 100644 index 000000000..562eddb7f --- /dev/null +++ b/lib/esbonio/esbonio/server/features/project_manager/manager.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import pathlib +import typing + +from esbonio import server +from esbonio.server import Uri + +from .project import Project + +if typing.TYPE_CHECKING: + from typing import Dict + from typing import Optional + from typing import Union + + +class ProjectManager(server.LanguageFeature): + """Responsible for managing project instances.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.projects: Dict[str, Project] = {} + """Holds active project instances""" + + def register_project(self, scope: str, dbpath: Union[str, pathlib.Path]): + """Register a project.""" + self.logger.debug("Registered project for scope '%s': '%s'", scope, dbpath) + self.projects[scope] = Project(dbpath) + + def get_project(self, uri: Uri) -> Optional[Project]: + """Return the project instance for the given uri, if available""" + scope = self.server.configuration.scope_for(uri) + + if (project := self.projects.get(scope, None)) is None: + self.logger.debug("No applicable project for uri: %s", uri) + return None + + return project diff --git a/lib/esbonio/esbonio/server/features/project_manager/project.py b/lib/esbonio/esbonio/server/features/project_manager/project.py new file mode 100644 index 000000000..2fde56ded --- /dev/null +++ b/lib/esbonio/esbonio/server/features/project_manager/project.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import json +import pathlib +import typing + +import aiosqlite + +from esbonio.server import Uri +from esbonio.sphinx_agent import types + +if typing.TYPE_CHECKING: + from typing import Any + from typing import Dict + from typing import List + from typing import Optional + from typing import Tuple + from typing import Union + + +class Project: + """Represents a documentation project.""" + + def __init__(self, dbpath: Union[str, pathlib.Path]): + self.dbpath = dbpath + self._connection: Optional[aiosqlite.Connection] = None + + async def close(self): + if self._connection is not None: + await self._connection.close() + + async def get_db(self) -> aiosqlite.Connection: + if self._connection is None: + self._connection = await aiosqlite.connect(self.dbpath) + + return self._connection + + async def get_src_uris(self) -> List[Uri]: + """Return all known source uris.""" + db = await self.get_db() + + query = "SELECT uri FROM files" + async with db.execute(query) as cursor: + results = await cursor.fetchall() + return [Uri.parse(s[0]) for s in results] + + async def get_build_path(self, src_uri: Uri) -> Optional[str]: + """Get the build path associated with the given ``src_uri``.""" + db = await self.get_db() + + query = "SELECT urlpath FROM files WHERE uri = ?" + async with db.execute(query, (str(src_uri.resolve()),)) as cursor: + if (result := await cursor.fetchone()) is None: + return None + + return result[0] + + async def get_config_value(self, name: str) -> Optional[Any]: + """Return the requested configuration value, if available.""" + + db = await self.get_db() + query = "SELECT value FROM config WHERE name = ?" + cursor = await db.execute(query, (name,)) + + if (row := await cursor.fetchone()) is None: + return None + + (value,) = row + return json.loads(value) + + async def get_directives(self) -> List[Tuple[str, Optional[str]]]: + """Get the directives known to Sphinx.""" + db = await self.get_db() + + query = "SELECT name, implementation FROM directives" + cursor = await db.execute(query) + return await cursor.fetchall() # type: ignore[return-value] + + async def get_document_symbols(self, src_uri: Uri) -> List[types.Symbol]: + """Get the symbols for the given file.""" + db = await self.get_db() + query = ( + "SELECT id, name, kind, detail, range, parent_id, order_id " + "FROM symbols WHERE uri = ?" + ) + cursor = await db.execute(query, (str(src_uri.resolve()),)) + return await cursor.fetchall() # type: ignore[return-value] + + async def find_symbols(self, **kwargs) -> List[types.Symbol]: + """Find symbols which match the given criteria.""" + db = await self.get_db() + base_query = ( + "SELECT id, name, kind, detail, range, parent_id, order_id FROM symbols" + ) + where: List[str] = [] + parameters: List[Any] = [] + + for param, value in kwargs.items(): + where.append(f"{param} = ?") + parameters.append(value) + + if where: + conditions = " AND ".join(where) + query = " ".join([base_query, "WHERE", conditions]) + else: + query = base_query + + cursor = await db.execute(query, tuple(parameters)) + return await cursor.fetchall() # type: ignore[return-value] + + async def get_workspace_symbols( + self, query: str + ) -> List[Tuple[str, str, int, str, str, str]]: + """Return all the workspace symbols matching the given query string""" + + db = await self.get_db() + sql_query = """\ +SELECT + child.uri, + child.name, + child.kind, + child.detail, + child.range, + COALESCE(parent.name, '') AS container_name +FROM + symbols child +LEFT JOIN + symbols parent ON (child.parent_id = parent.id AND child.uri = parent.uri) +WHERE + child.name like ? or child.detail like ?;""" + + query_str = f"%{query}%" + cursor = await db.execute(sql_query, (query_str, query_str)) + return await cursor.fetchall() # type: ignore[return-value] + + async def get_diagnostics(self) -> Dict[Uri, List[Dict[str, Any]]]: + """Get diagnostics for the project.""" + db = await self.get_db() + cursor = await db.execute("SELECT * FROM diagnostics") + results: Dict[Uri, List[Dict[str, Any]]] = {} + + for uri_str, item in await cursor.fetchall(): + uri = Uri.parse(uri_str) + diagnostic = json.loads(item) + results.setdefault(uri, []).append(diagnostic) + + return results diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/__init__.py b/lib/esbonio/esbonio/server/features/sphinx_manager/__init__.py index 36bb44009..da73648ad 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/__init__.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/__init__.py @@ -1,23 +1,22 @@ from __future__ import annotations from esbonio.server import EsbonioLanguageServer +from esbonio.server.features.project_manager import ProjectManager +from .client import ClientState from .client import SphinxClient -from .client_mock import MockSphinxClient -from .client_mock import mock_sphinx_client_factory from .client_subprocess import make_subprocess_sphinx_client from .config import SphinxConfig from .manager import SphinxManager __all__ = [ + "ClientState", "SphinxClient", "SphinxConfig", "SphinxManager", - "MockSphinxClient", - "mock_sphinx_client_factory", ] -def esbonio_setup(server: EsbonioLanguageServer): - manager = SphinxManager(make_subprocess_sphinx_client, server) +def esbonio_setup(server: EsbonioLanguageServer, project_manager: ProjectManager): + manager = SphinxManager(make_subprocess_sphinx_client, project_manager, server) server.add_feature(manager) diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/client.py b/lib/esbonio/esbonio/server/features/sphinx_manager/client.py index d593b70a9..78b5d0892 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/client.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/client.py @@ -1,61 +1,84 @@ from __future__ import annotations +import enum import typing from typing import Protocol if typing.TYPE_CHECKING: + import pathlib from typing import Any from typing import Dict + from typing import Generator from typing import List from typing import Optional - from typing import Tuple - - import aiosqlite from esbonio.server import Uri from esbonio.sphinx_agent import types - from .config import SphinxConfig + +class ClientState(enum.Enum): + """The set of possible states the client may be in.""" + + Starting = enum.auto() + """The client is starting.""" + + Running = enum.auto() + """The client is running normally.""" + + Building = enum.auto() + """The client is currently building.""" + + Errored = enum.auto() + """The client has enountered some unrecoverable error and should not be used.""" + + Exited = enum.auto() + """The client is no longer running.""" class SphinxClient(Protocol): """Describes the API language features can use to inspect/manipulate a Sphinx application instance.""" + state: Optional[ClientState] + sphinx_info: Optional[types.SphinxInfo] + @property - def id(self) -> Optional[str]: + def id(self) -> str: """The id of the Sphinx instance.""" + ... @property - def db(self) -> Optional[aiosqlite.Connection]: + def db(self) -> pathlib.Path: """Connection to the associated database.""" @property - def builder(self) -> Optional[str]: + def builder(self) -> str: """The name of the Sphinx builder.""" + ... @property - def building(self) -> bool: - """Indicates if the Sphinx instance is building.""" - - @property - def build_uri(self) -> Optional[Uri]: + def build_uri(self) -> Uri: """The URI to the Sphinx application's build dir.""" + ... @property - def conf_uri(self) -> Optional[Uri]: + def conf_uri(self) -> Uri: """The URI to the Sphinx application's conf dir.""" + ... @property - def src_uri(self) -> Optional[Uri]: + def src_uri(self) -> Uri: """The URI to the Sphinx application's src dir.""" + ... - async def start(self, config: SphinxConfig): - """Start the client.""" + def __await__(self) -> Generator[Any, None, SphinxClient]: + """A SphinxClient should be awaitable""" ... - async def create_application(self, config: SphinxConfig) -> types.SphinxInfo: - """Create a new Sphinx application instance.""" + def add_listener(self, event: str, handler): ... + + async def start(self) -> SphinxClient: + """Start the client.""" ... async def build( @@ -68,37 +91,5 @@ async def build( """Trigger a Sphinx build.""" ... - async def get_src_uris(self) -> List[Uri]: - """Return all known source files.""" - ... - - async def get_build_path(self, src_uri: Uri) -> Optional[str]: - """Get the build path associated with the given ``src_uri``.""" - - async def get_config_value(self, name: str) -> Optional[Any]: - """Return the requested configuration value, if available.""" - - async def get_diagnostics(self) -> Dict[Uri, List[Dict[str, Any]]]: - """Get the diagnostics for the project.""" - ... - - async def get_directives(self) -> List[Tuple[str, Optional[str]]]: - """Get all the directives known to Sphinx.""" - ... - - async def get_document_symbols(self, src_uri: Uri) -> List[types.Symbol]: - """Get the symbols for the given file.""" - ... - - async def find_symbols(self, **kwargs) -> List[types.Symbol]: - """Find symbols which match the given criteria.""" - ... - - async def get_workspace_symbols( - self, query: str - ) -> List[Tuple[str, str, int, str, str, str]]: - """Return all the workspace symbols matching the given query string""" - ... - async def stop(self): """Stop the client.""" diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/client_mock.py b/lib/esbonio/esbonio/server/features/sphinx_manager/client_mock.py deleted file mode 100644 index 1836fb207..000000000 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/client_mock.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -import asyncio -import typing -from typing import Dict -from typing import List -from typing import Optional -from typing import Union - -from esbonio.sphinx_agent import types - -if typing.TYPE_CHECKING: - from esbonio.server import Uri - - from .client import SphinxClient - from .manager import SphinxManager - - -class MockSphinxClient: - """A mock SphinxClient implementation, used for testing.""" - - def __init__( - self, - create_result: Union[types.SphinxInfo, Exception], - build_result: Optional[Union[types.BuildResult, Exception]] = None, - build_file_map: Optional[Dict[Uri, str]] = None, - startup_delay: float = 0.5, - ): - """Create a new instance. - - Parameters - ---------- - create_result - The result to return when calling ``create_application``. - If an exception is given it will be raised. - - build_file_map - The build file map to use. - - build_result - The result to return when calling ``build``. - If an exception is given it will be raised. - - startup_delay - The duration to sleep for when calling ``start`` - """ - self._create_result = create_result - self._startup_delay = startup_delay - self._build_result = build_result or types.BuildResult() - self._build_file_map = build_file_map or {} - self.building = False - - @property - def id(self) -> Optional[str]: - """The id of the Sphinx instance.""" - if isinstance(self._create_result, Exception): - return None - - return self._create_result.id - - async def start(self, *args, **kwargs): - await asyncio.sleep(self._startup_delay) - - async def stop(self, *args, **kwargs): - pass - - async def create_application(self, *args, **kwargs) -> types.SphinxInfo: - """Create an application.""" - - if isinstance(self._create_result, Exception): - raise self._create_result - - return self._create_result - - async def build(self, *args, **kwargs) -> types.BuildResult: - """Trigger a build""" - - if isinstance(self._build_result, Exception): - raise self._build_result - - return self._build_result - - async def get_src_uris(self) -> List[Uri]: - """Return all known source files.""" - return [s for s in self._build_file_map.keys()] - - async def get_build_path(self, src_uri: Uri) -> Optional[str]: - """Get the build path associated with the given ``src_uri``.""" - return self._build_file_map.get(src_uri) - - -def mock_sphinx_client_factory(client: Optional[SphinxClient] = None): - """Return a factory function that can be used with a ``SphinxManager`` instance.""" - - def factory(manager: SphinxManager): - if client is None: - raise RuntimeError("Unexpected client creation") - return client - - return factory diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py b/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py index b901e3faf..5cb93112b 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py @@ -1,20 +1,24 @@ """Subprocess implementation of the ``SphinxClient`` protocol.""" + from __future__ import annotations import asyncio -import json import logging import os +import pathlib import subprocess +import sys import typing +from uuid import uuid4 -import aiosqlite from pygls.client import JsonRPCClient from pygls.protocol import JsonRPCProtocol import esbonio.sphinx_agent.types as types +from esbonio.server import EventSource from esbonio.server import Uri +from .client import ClientState from .config import SphinxConfig if typing.TYPE_CHECKING: @@ -22,12 +26,14 @@ from typing import Dict from typing import List from typing import Optional - from typing import Tuple from .client import SphinxClient from .manager import SphinxManager +sphinx_logger = logging.getLogger("sphinx") + + class SphinxAgentProtocol(JsonRPCProtocol): """Describes the protocol spoken between the client below and the sphinx agent.""" @@ -47,78 +53,114 @@ class SubprocessSphinxClient(JsonRPCClient): def __init__( self, + config: SphinxConfig, logger: Optional[logging.Logger] = None, protocol_cls=SphinxAgentProtocol, *args, **kwargs, ): super().__init__(protocol_cls=protocol_cls, *args, **kwargs) # type: ignore[misc] + + self.id = str(uuid4()) + """The client's id.""" + + self.config = config + """Configuration values.""" + self.logger = logger or logging.getLogger(__name__) + """The logger instance to use.""" self.sphinx_info: Optional[types.SphinxInfo] = None + """Information about the Sphinx application the client is connected to.""" - self._connection: Optional[aiosqlite.Connection] = None - self._building = False + self.state: Optional[ClientState] = None + """The current state of the client.""" - @property - def converter(self): - return self.protocol._converter + self.exception: Optional[Exception] = None + """The most recently encountered exception (if any)""" - @property - def id(self) -> Optional[str]: - """The id of the Sphinx instance.""" - if self.sphinx_info is None: - return None + self._events = EventSource(self.logger) + """The sphinx client can emit events.""" + + self._startup_task: Optional[asyncio.Task] = None + """The startup task.""" + + self._stderr_forwarder: Optional[asyncio.Task] = None + """A task that forwards the server's stderr to the test process.""" + + def __repr__(self): + if self.state is None: + return "SphinxClient<None>" + + if self.state == ClientState.Errored: + return f"SphinxClient<{self.state.name}: {self.exception}>" - return self.sphinx_info.id + state = self.state.name + command = " ".join(self.config.build_command) + return f"SphinxClient<{state}: {command}>" + + def __await__(self): + """Makes the client await-able""" + if self._startup_task is None: + self._startup_task = asyncio.create_task(self.start()) + + return self._startup_task.__await__() @property - def building(self) -> bool: - return self._building + def converter(self): + return self.protocol._converter @property - def builder(self) -> Optional[str]: + def builder(self) -> str: """The sphinx application's builder name""" if self.sphinx_info is None: - return None + raise RuntimeError("sphinx_info is None, has the client been started?") return self.sphinx_info.builder_name @property - def src_uri(self) -> Optional[Uri]: + def src_uri(self) -> Uri: """The src uri of the Sphinx application.""" if self.sphinx_info is None: - return None + raise RuntimeError("sphinx_info is None, has the client been started?") return Uri.for_file(self.sphinx_info.src_dir) @property - def conf_uri(self) -> Optional[Uri]: + def conf_uri(self) -> Uri: """The conf uri of the Sphinx application.""" if self.sphinx_info is None: - return None + raise RuntimeError("sphinx_info is None, has the client been started?") return Uri.for_file(self.sphinx_info.conf_dir) @property - def db(self) -> Optional[aiosqlite.Connection]: + def db(self) -> pathlib.Path: """Connection to the associated database.""" - return self._connection + if self.sphinx_info is None: + raise RuntimeError("sphinx_info is None, has the client been started?") + + return pathlib.Path(self.sphinx_info.dbpath) @property - def build_uri(self) -> Optional[Uri]: + def build_uri(self) -> Uri: """The build uri of the Sphinx application.""" if self.sphinx_info is None: - return None + raise RuntimeError("sphinx_info is None, has the client been started?") return Uri.for_file(self.sphinx_info.build_dir) + def add_listener(self, event: str, handler): + self._events.add_listener(event, handler) + async def server_exit(self, server: asyncio.subprocess.Process): """Called when the sphinx agent process exits.""" # 0: all good # -15: terminated if server.returncode not in {0, -15}: + self.exception = RuntimeError(server.returncode) + self._set_state(ClientState.Errored) self.logger.error( f"sphinx-agent process exited with code: {server.returncode}" ) @@ -135,65 +177,71 @@ async def server_exit(self, server: asyncio.subprocess.Process): "%s future '%s' for pending request '%s'", message, fut, id_ ) - async def start(self, config: SphinxConfig): + if self.state != ClientState.Errored: + self._set_state(ClientState.Exited) + + async def start_io(self, cmd: str, *args, **kwargs): + await super().start_io(cmd, *args, **kwargs) + + # Forward the server's stderr to this process' stderr + if self._server and self._server.stderr: + self._stderr_forwarder = asyncio.create_task(forward_stderr(self._server)) + + async def start(self) -> SphinxClient: """Start the client.""" - if len(config.python_command) == 0: - raise ValueError("No python environment configured") + # Only try starting once. + if self.state is not None: + return self - command = [] + try: + self._set_state(ClientState.Starting) + command = get_start_command(self.config, self.logger) + env = get_sphinx_env(self.config) - if config.enable_dev_tools and ( - lsp_devtools := self._get_lsp_devtools_command() - ): - command.extend([lsp_devtools, "agent", "--"]) + self.logger.debug("Starting sphinx agent: %s", " ".join(command)) + await self.start_io(*command, env=env, cwd=self.config.cwd) - command.extend([*config.python_command, "-m", "sphinx_agent"]) - env = get_sphinx_env(config) + params = types.CreateApplicationParams( + command=self.config.build_command, + enable_sync_scrolling=self.config.enable_sync_scrolling, + ) - self.logger.debug("Starting sphinx agent: %s", " ".join(command)) - await self.start_io(*command, env=env, cwd=config.cwd) + self.sphinx_info = await self.protocol.send_request_async( + "sphinx/createApp", params + ) - def _get_lsp_devtools_command(self) -> Optional[str]: - # Assumes that the user has `lsp-devtools` available on their PATH - # TODO: Windows support - result = subprocess.run(["command", "-v", "lsp-devtools"], capture_output=True) - if result.returncode == 0: - lsp_devtools = result.stdout.decode("utf8").strip() - return lsp_devtools + self._set_state(ClientState.Running) + return self + except Exception as exc: + self.logger.debug("Unable to start SphinxClient: %s", exc, exc_info=True) - stderr = result.stderr.decode("utf8").strip() - self.logger.debug("Unable to locate lsp-devtools command", stderr) - return None + self.exception = exc + self._set_state(ClientState.Errored) - async def stop(self): - """Stop the client.""" + return self - self.protocol.notify("exit", None) - if self._connection: - await self._connection.close() + def _set_state(self, new_state: ClientState): + """Change the state of the client.""" + old_state, self.state = self.state, new_state - # Give the agent a little time to close. - # await asyncio.sleep(0.5) - await super().stop() + self.logger.debug("SphinxClient[%s]: %s -> %s", self.id, old_state, new_state) + self._events.trigger("state-change", self, old_state, new_state) - async def create_application(self, config: SphinxConfig) -> types.SphinxInfo: - """Create a sphinx application object.""" + async def stop(self): + """Stop the client.""" - params = types.CreateApplicationParams( - command=config.build_command, - enable_sync_scrolling=config.enable_sync_scrolling, - ) + if self.state in {ClientState.Running, ClientState.Building}: + self.protocol.notify("exit", None) - sphinx_info = await self.protocol.send_request_async("sphinx/createApp", params) - self.sphinx_info = sphinx_info + # Give the agent a little time to close. + await asyncio.sleep(0.5) - try: - self._connection = await aiosqlite.connect(sphinx_info.dbpath) - except Exception: - self.logger.error("Unable to connect to database", exc_info=True) + if self._stderr_forwarder: + self._stderr_forwarder.cancel() - return sphinx_info + self.logger.debug(self._async_tasks) + await super().stop() async def build( self, @@ -218,133 +266,19 @@ async def build( return result - async def get_src_uris(self) -> List[Uri]: - """Return all known source uris.""" - - if self.db is None: - return [] - - query = "SELECT uri FROM files" - async with self.db.execute(query) as cursor: - results = await cursor.fetchall() - return [Uri.parse(s[0]) for s in results] - - async def get_build_path(self, src_uri: Uri) -> Optional[str]: - """Get the build path associated with the given ``src_uri``.""" - - if self.db is None: - return None - - query = "SELECT urlpath FROM files WHERE uri = ?" - async with self.db.execute(query, (str(src_uri.resolve()),)) as cursor: - if (result := await cursor.fetchone()) is None: - return None - - return result[0] - - async def get_config_value(self, name: str) -> Optional[Any]: - """Return the requested configuration value, if available.""" - if self.db is None: - return None - - query = "SELECT value FROM config WHERE name = ?" - cursor = await self.db.execute(query, (name,)) - - if (row := await cursor.fetchone()) is None: - return None - - (value,) = row - return json.loads(value) - - async def get_directives(self) -> List[Tuple[str, Optional[str]]]: - """Get the directives known to Sphinx.""" - if self.db is None: - return [] - - query = "SELECT name, implementation FROM directives" - cursor = await self.db.execute(query) - return await cursor.fetchall() # type: ignore[return-value] - - async def get_document_symbols(self, src_uri: Uri) -> List[types.Symbol]: - """Get the symbols for the given file.""" - if self.db is None: - return [] - query = ( - "SELECT id, name, kind, detail, range, parent_id, order_id " - "FROM symbols WHERE uri = ?" - ) - cursor = await self.db.execute(query, (str(src_uri.resolve()),)) - return await cursor.fetchall() # type: ignore[return-value] - - async def find_symbols(self, **kwargs) -> List[types.Symbol]: - """Find symbols which match the given criteria.""" - if self.db is None: - return [] +async def forward_stderr(server: asyncio.subprocess.Process): + if server.stderr is None: + return - base_query = ( - "SELECT id, name, kind, detail, range, parent_id, order_id FROM symbols" - ) - where: List[str] = [] - parameters: List[Any] = [] + # EOF is signalled with an empty bytestring + while (line := await server.stderr.readline()) != b"": + sphinx_logger.info(line.decode()) - for param, value in kwargs.items(): - where.append(f"{param} = ?") - parameters.append(value) - if where: - conditions = " AND ".join(where) - query = " ".join([base_query, "WHERE", conditions]) - else: - query = base_query - - cursor = await self.db.execute(query, tuple(parameters)) - return await cursor.fetchall() # type: ignore[return-value] - - async def get_workspace_symbols( - self, query: str - ) -> List[Tuple[str, str, int, str, str, str]]: - """Return all the workspace symbols matching the given query string""" - - if self.db is None: - return [] - - sql_query = """\ -SELECT - child.uri, - child.name, - child.kind, - child.detail, - child.range, - COALESCE(parent.name, '') AS container_name -FROM - symbols child -LEFT JOIN - symbols parent ON (child.parent_id = parent.id AND child.uri = parent.uri) -WHERE - child.name like ? or child.detail like ?;""" - - query_str = f"%{query}%" - cursor = await self.db.execute(sql_query, (query_str, query_str)) - return await cursor.fetchall() # type: ignore[return-value] - - async def get_diagnostics(self) -> Dict[Uri, List[Dict[str, Any]]]: - """Get diagnostics for the project.""" - if self.db is None: - return {} - - cursor = await self.db.execute("SELECT * FROM diagnostics") - results: Dict[Uri, List[Dict[str, Any]]] = {} - - for uri_str, item in await cursor.fetchall(): - uri = Uri.parse(uri_str) - diagnostic = json.loads(item) - results.setdefault(uri, []).append(diagnostic) - - return results - - -def make_subprocess_sphinx_client(manager: SphinxManager) -> SphinxClient: +def make_subprocess_sphinx_client( + manager: SphinxManager, config: SphinxConfig +) -> SphinxClient: """Factory function for creating a ``SubprocessSphinxClient`` instance. Parameters @@ -352,16 +286,19 @@ def make_subprocess_sphinx_client(manager: SphinxManager) -> SphinxClient: manager The manager instance creating the client + config + The Sphinx configuration + Returns ------- SphinxClient The configured client """ - client = SubprocessSphinxClient(logger=manager.logger) + client = SubprocessSphinxClient(config, logger=manager.logger) @client.feature("window/logMessage") def _on_msg(ls: SubprocessSphinxClient, params): - manager.server.show_message_log(params.message) + sphinx_logger.info(params.message) @client.feature("$/progress") def _on_progress(ls: SubprocessSphinxClient, params): @@ -370,17 +307,17 @@ def _on_progress(ls: SubprocessSphinxClient, params): return client -def make_test_sphinx_client() -> SubprocessSphinxClient: +def make_test_sphinx_client(config: SphinxConfig) -> SubprocessSphinxClient: """Factory function for creating a ``SubprocessSphinxClient`` instance to use for testing.""" logger = logging.getLogger("sphinx_client") logger.setLevel(logging.INFO) - client = SubprocessSphinxClient() + client = SubprocessSphinxClient(config) @client.feature("window/logMessage") def _(params): - logger.info("%s", params.message) + print(params.message, file=sys.stderr) @client.feature("$/progress") def _on_progress(params): @@ -393,6 +330,7 @@ def get_sphinx_env(config: SphinxConfig) -> Dict[str, str]: """Return the set of environment variables to use with the Sphinx process.""" env = { + "PYTHONUNBUFFERED": "1", "PYTHONPATH": os.pathsep.join([str(p) for p in config.python_path]), } for envname, value in os.environ.items(): @@ -403,3 +341,27 @@ def get_sphinx_env(config: SphinxConfig) -> Dict[str, str]: env[envname] = value return env + + +def get_start_command(config: SphinxConfig, logger: logging.Logger): + """Return the command to use to start the sphinx agent.""" + + command = [] + + if len(config.python_command) == 0: + raise ValueError("No python environment configured") + + if config.enable_dev_tools: + # Assumes that the user has `lsp-devtools` available on their PATH + # TODO: Windows support + result = subprocess.run(["command", "-v", "lsp-devtools"], capture_output=True) + if result.returncode == 0: + lsp_devtools = result.stdout.decode("utf8").strip() + command.extend([lsp_devtools, "agent", "--"]) + + else: + stderr = result.stderr.decode("utf8").strip() + logger.debug("Unable to locate lsp-devtools command", stderr) + + command.extend([*config.python_command, "-m", "sphinx_agent"]) + return command diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/config.py b/lib/esbonio/esbonio/server/features/sphinx_manager/config.py index a21cf5d87..ba92489f0 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/config.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/config.py @@ -141,18 +141,23 @@ def _resolve_cwd( if self.cwd: return self.cwd - for folder_uri in workspace.folders.keys(): - folder_uri = Uri.parse(folder_uri) - if folder_uri and str(uri).startswith(str(folder_uri)): - break - else: - folder_uri = Uri.parse(workspace.root_uri) - - if folder_uri is None or (cwd := folder_uri.fs_path) is None: - logger.error("Unable to determine working directory from '%s'", folder_uri) - return None + candidates = [Uri.parse(f) for f in workspace.folders.keys()] + + if workspace.root_uri is not None: + if (root_uri := Uri.parse(workspace.root_uri)) not in candidates: + candidates.append(root_uri) + + for folder in candidates: + if str(uri).startswith(str(folder)): + if (cwd := folder.fs_path) is None: + logger.error( + "Unable to determine working directory from '%s'", folder + ) + return None - return cwd + return cwd + + return None def _resolve_python_path(self, logger: logging.Logger) -> List[pathlib.Path]: """Return the list of paths to put on the sphinx agent's ``PYTHONPATH`` @@ -161,9 +166,6 @@ def _resolve_python_path(self, logger: logging.Logger) -> List[pathlib.Path]: packages into the user's Python environment. This method will locate the installation path of the sphinx agent and return it. - Additionally if the ``enable_dev_tools`` flag is set, this will attempt to - locate the ``lsp_devtools`` package - Parameters ---------- logger diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/manager.py b/lib/esbonio/esbonio/server/features/sphinx_manager/manager.py index c00759c0e..a52307ab0 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/manager.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/manager.py @@ -1,61 +1,122 @@ from __future__ import annotations import asyncio -import inspect +import traceback import typing import uuid -from typing import Callable -from typing import Dict -from typing import Optional +from functools import partial +import attrs import lsprotocol.types as lsp -import esbonio.sphinx_agent.types as types -from esbonio.server import LanguageFeature +from esbonio import server from esbonio.server import Uri +from esbonio.sphinx_agent import types +from .client import ClientState from .config import SphinxConfig if typing.TYPE_CHECKING: + from typing import Callable + from typing import Dict + from typing import Optional + + from esbonio.server.features.project_manager import ProjectManager + from .client import SphinxClient + SphinxClientFactory = Callable[["SphinxManager", "SphinxConfig"], "SphinxClient"] + + +@attrs.define +class ClientCreatedNotification: + """The payload of a ``sphinx/clientCreated`` notification""" + + id: str + """The client's id""" + + scope: str + """The scope at which the client was created.""" + + config: SphinxConfig + """The final configuration.""" + + +@attrs.define +class AppCreatedNotification: + """The payload of a ``sphinx/appCreated`` notification""" + + id: str + """The client's id""" + + application: types.SphinxInfo + """Details about the created application.""" + -SphinxClientFactory = Callable[["SphinxManager"], "SphinxClient"] +@attrs.define +class ClientErroredNotification: + """The payload of a ``sphinx/clientErrored`` notification""" + id: str + """The client's id""" -class SphinxManager(LanguageFeature): + error: str + """Short description of the error.""" + + detail: str + """Detailed description of the error.""" + + +@attrs.define +class ClientDestroyedNotification: + """The payload of ``sphinx/clientDestroyed`` notification.""" + + id: str + """The client's id""" + + +class SphinxManager(server.LanguageFeature): """Responsible for managing Sphinx application instances.""" - def __init__(self, client_factory: SphinxClientFactory, *args, **kwargs): + def __init__( + self, + client_factory: SphinxClientFactory, + project_manager: ProjectManager, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.client_factory = client_factory """Used to create new Sphinx client instances.""" - self.clients: Dict[Uri, SphinxClient] = {} + self.project_manager = project_manager + """The project manager instance to use.""" + + self.clients: Dict[str, Optional[SphinxClient]] = { + # Prevent any clients from being created in the global scope. + "": None, + } """Holds currently active Sphinx clients.""" - self.handlers: Dict[str, set] = {} - """Collection of handlers for various events.""" + self._events = server.EventSource(self.logger) + """The SphinxManager can emit events.""" self._pending_builds: Dict[str, asyncio.Task] = {} """Holds tasks that will trigger a build after a given delay if not cancelled.""" - self._client_creating: Optional[asyncio.Task] = None - """If set, indicates we're in the process of setting up a new client.""" - self._progress_tokens: Dict[str, str] = {} """Holds work done progress tokens.""" def add_listener(self, event: str, handler): - self.handlers.setdefault(event, set()).add(handler) + self._events.add_listener(event, handler) async def document_change(self, params: lsp.DidChangeTextDocumentParams): if (uri := Uri.parse(params.text_document.uri)) is None: return client = await self.get_client(uri) - if client is None or client.id is None: + if client is None: return # Cancel any existing pending builds @@ -77,7 +138,7 @@ async def document_save(self, params: lsp.DidSaveTextDocumentParams): return client = await self.get_client(uri) - if client is None or client.id is None: + if client is None: return # Cancel any existing pending builds @@ -86,6 +147,18 @@ async def document_save(self, params: lsp.DidSaveTextDocumentParams): await self.trigger_build(uri) + async def shutdown(self, params: None): + """Called when the server is instructed to ``shutdown``.""" + + # Stop any existing clients. + tasks = [] + for client in self.clients.values(): + if client: + self.logger.debug("Stopping SphinxClient: %s", client) + tasks.append(asyncio.create_task(client.stop())) + + await asyncio.gather(*tasks) + async def trigger_build_after(self, uri: Uri, app_id: str, delay: float): """Trigger a build for the given uri after the given delay.""" await asyncio.sleep(delay) @@ -96,21 +169,25 @@ async def trigger_build_after(self, uri: Uri, app_id: str, delay: float): async def trigger_build(self, uri: Uri): """Trigger a build for the relevant Sphinx application for the given uri.""" + client = await self.get_client(uri) - if client is None or client.building: + if client is None or client.state != ClientState.Running: + return + + if (project := self.project_manager.get_project(uri)) is None: return # Pass through any unsaved content to the Sphinx agent. content_overrides: Dict[str, str] = {} - known_src_uris = await client.get_src_uris() + known_src_uris = await project.get_src_uris() for src_uri in known_src_uris: doc = self.server.workspace.get_document(str(src_uri)) doc_version = doc.version or 0 saved_version = getattr(doc, "saved_version", 0) - if saved_version < doc_version and (fs_path := src_uri.fs_path) is not None: - content_overrides[fs_path] = doc.source + if saved_version < doc_version: + content_overrides[str(src_uri)] = doc.source await self.start_progress(client) @@ -123,101 +200,106 @@ async def trigger_build(self, uri: Uri): self.stop_progress(client) # Notify listeners. - for listener in self.handlers.get("build", set()): - try: - # TODO: Concurrent awaiting? - res = listener(client, result) - if inspect.isawaitable(res): - await res - except Exception: - name = f"{listener}" - self.logger.error("Error in build handler '%s'", name, exc_info=True) + self._events.trigger("build", client, result) async def get_client(self, uri: Uri) -> Optional[SphinxClient]: """Given a uri, return the relevant sphinx client instance for it.""" - # Wait until the new client is created - it might be the one we're looking for! - if self._client_creating: - await self._client_creating - - # Always check the fully resolved uri. - resolved_uri = uri.resolve() - - for src_uri, client in self.clients.items(): - if resolved_uri in (await client.get_src_uris()): - return client - - # For now assume a single client instance per srcdir. - # This *should* prevent us from spwaning endless client instances - # when given a file located near a valid Sphinx project - but not actually - # part of it. - in_src_dir = str(resolved_uri).startswith(str(src_uri)) - if in_src_dir: - # Of course, we can only tell if a uri truly is not in a project - # when the build file map is populated! - # if len(client.build_file_map) == 0: - return client - - # Create a new client instance. - self._client_creating = asyncio.create_task(self._create_client(uri)) - try: - return await self._client_creating - finally: - # Be sure to unset the task when it resolves - self._client_creating = None - - async def _create_client(self, uri: Uri) -> Optional[SphinxClient]: - """Create a new sphinx client instance.""" - # TODO: Replace with config subscription - await self.server.ready - config = self.server.configuration.get( - "esbonio.sphinx", SphinxConfig, scope=uri - ) - if config is None: + scope = self.server.configuration.scope_for(uri) + if scope not in self.clients: + self.logger.debug("No client found, creating new subscription") + self.server.configuration.subscribe( + "esbonio.sphinx", + SphinxConfig, + self._create_or_replace_client, + scope=uri, + ) + # The first few callers in a given scope will miss out, but that shouldn't matter + # too much return None - resolved = config.resolve(uri, self.server.workspace, self.logger) - if resolved is None: + if (client := self.clients[scope]) is None: + self.logger.debug("No applicable client for uri: %s", uri) return None - client = self.client_factory(self) + return await client - try: - await client.start(resolved) - except Exception as exc: - message = "Unable to start sphinx-agent" - self.logger.error(message, exc_info=True) - self.server.show_message(f"{message}: {exc}", lsp.MessageType.Error) + async def _create_or_replace_client( + self, event: server.ConfigChangeEvent[SphinxConfig] + ): + """Create or replace thesphinx client instance for the given config.""" - return None + config = event.value - try: - sphinx_info = await client.create_application(resolved) - except Exception as exc: - message = "Unable to create sphinx application" - self.logger.error(message, exc_info=True) - self.server.show_message(f"{message}: {exc}", lsp.MessageType.Error) + # Do not try and create clients in the global scope + if event.scope == "": + return - await client.stop() - return None + # If there was a previous client, stop it. + if (previous_client := self.clients.pop(event.scope, None)) is not None: + self.server.lsp.notify( + "sphinx/clientDestroyed", + ClientDestroyedNotification(id=previous_client.id), + ) + self.server.run_task(previous_client.stop()) - if client.src_uri is None: - self.logger.error("No src uri!") - await client.stop() - return None + resolved = config.resolve( + Uri.parse(event.scope), self.server.workspace, self.logger + ) + if resolved is None: + self.clients[event.scope] = None + return - self.server.lsp.notify("sphinx/appCreated", sphinx_info) - self.clients[client.src_uri] = client - return client + self.clients[event.scope] = client = self.client_factory(self, resolved) + client.add_listener("state-change", partial(self._on_state_change, event.scope)) + + self.server.lsp.notify( + "sphinx/clientCreated", + ClientCreatedNotification(id=client.id, scope=event.scope, config=resolved), + ) + self.logger.debug("Client created for scope %s", event.scope) + + # Start the client + await client + + def _on_state_change( + self, + scope: str, + client: SphinxClient, + old_state: ClientState, + new_state: ClientState, + ): + """React to state changes in the client.""" + + if old_state == ClientState.Starting and new_state == ClientState.Running: + if (sphinx_info := client.sphinx_info) is not None: + self.project_manager.register_project(scope, client.db) + self.server.lsp.notify( + "sphinx/appCreated", + AppCreatedNotification(id=client.id, application=sphinx_info), + ) + + if new_state == ClientState.Errored: + error = "" + detail = "" + + if (exc := getattr(client, "exception", None)) is not None: + error = f"{type(exc).__name__}: {exc}" + detail = "".join( + traceback.format_exception(type(exc), exc, exc.__traceback__) + ) + + self.server.lsp.show_message(error, lsp.MessageType.Error) + self.server.lsp.notify( + "sphinx/clientErrored", + ClientErroredNotification(id=client.id, error=error, detail=detail), + ) async def start_progress(self, client: SphinxClient): """Start reporting work done progress for the given client.""" - if client.id is None: - return - token = str(uuid.uuid4()) - self.logger.error("Starting progress: '%s'", token) + self.logger.debug("Starting progress: '%s'", token) try: await self.server.progress.create_async(token) @@ -232,9 +314,6 @@ async def start_progress(self, client: SphinxClient): ) def stop_progress(self, client: SphinxClient): - if client.id is None: - return - if (token := self._progress_tokens.pop(client.id, None)) is None: return @@ -243,7 +322,7 @@ def stop_progress(self, client: SphinxClient): def report_progress(self, client: SphinxClient, progress: types.ProgressParams): """Report progress done for the given client.""" - if client.id is None: + if client.state not in {ClientState.Running, ClientState.Building}: return if (token := self._progress_tokens.get(client.id, None)) is None: diff --git a/lib/esbonio/esbonio/server/features/sphinx_support/diagnostics.py b/lib/esbonio/esbonio/server/features/sphinx_support/diagnostics.py index 55a71e0d7..7c46b6ffe 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_support/diagnostics.py +++ b/lib/esbonio/esbonio/server/features/sphinx_support/diagnostics.py @@ -3,18 +3,25 @@ from lsprotocol import types from esbonio.server import EsbonioLanguageServer +from esbonio.server.features.project_manager import ProjectManager from esbonio.server.features.sphinx_manager import SphinxClient from esbonio.server.features.sphinx_manager import SphinxManager async def refresh_diagnostics( - server: EsbonioLanguageServer, client: SphinxClient, result + server: EsbonioLanguageServer, + projects: ProjectManager, + client: SphinxClient, + result, ): """Refresh sphinx diagnostics.""" + if (project := projects.get_project(client.src_uri)) is None: + return + # TODO: Per-client id. server.clear_diagnostics("sphinx") - collection = await client.get_diagnostics() + collection = await project.get_diagnostics() for uri, items in collection.items(): diagnostics = [ server.converter.structure(item, types.Diagnostic) for item in items @@ -24,5 +31,11 @@ async def refresh_diagnostics( server.sync_diagnostics() -def esbonio_setup(server: EsbonioLanguageServer, sphinx_manager: SphinxManager): - sphinx_manager.add_listener("build", partial(refresh_diagnostics, server)) +def esbonio_setup( + server: EsbonioLanguageServer, + sphinx_manager: SphinxManager, + project_manager: ProjectManager, +): + sphinx_manager.add_listener( + "build", partial(refresh_diagnostics, server, project_manager) + ) diff --git a/lib/esbonio/esbonio/server/features/sphinx_support/directives.py b/lib/esbonio/esbonio/server/features/sphinx_support/directives.py index ab1abc0a6..861b19b72 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_support/directives.py +++ b/lib/esbonio/esbonio/server/features/sphinx_support/directives.py @@ -6,7 +6,7 @@ from esbonio import server from esbonio.server.features import directives -from esbonio.server.features.sphinx_manager import SphinxManager +from esbonio.server.features.project_manager import ProjectManager if typing.TYPE_CHECKING: from typing import List @@ -16,7 +16,7 @@ class SphinxDirectives(directives.DirectiveProvider): """Support for directives in a sphinx project.""" - def __init__(self, manager: SphinxManager): + def __init__(self, manager: ProjectManager): self.manager = manager async def suggest_directives( @@ -24,11 +24,11 @@ async def suggest_directives( ) -> Optional[List[directives.Directive]]: """Given a completion context, suggest directives that may be used.""" - if (client := await self.manager.get_client(context.uri)) is None: + if (project := self.manager.get_project(context.uri)) is None: return None # Does the document have a default domain set? - results = await client.find_symbols( + results = await project.find_symbols( uri=str(context.uri.resolve()), kind=types.SymbolKind.Class.value, detail="default-domain", @@ -38,11 +38,11 @@ async def suggest_directives( else: default_domain = None - primary_domain = await client.get_config_value("primary_domain") + primary_domain = await project.get_config_value("primary_domain") active_domain = default_domain or primary_domain or "py" result: List[directives.Directive] = [] - for name, implementation in await client.get_directives(): + for name, implementation in await project.get_directives(): # std: directives can be used unqualified if name.startswith("std:"): short_name = name.replace("std:", "") @@ -65,8 +65,8 @@ async def suggest_directives( def esbonio_setup( - sphinx_manager: SphinxManager, + project_manager: ProjectManager, directive_feature: directives.DirectiveFeature, ): - provider = SphinxDirectives(sphinx_manager) + provider = SphinxDirectives(project_manager) directive_feature.add_provider(provider) diff --git a/lib/esbonio/esbonio/server/features/sphinx_support/symbols.py b/lib/esbonio/esbonio/server/features/sphinx_support/symbols.py index 194d80b1b..c50413ad7 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_support/symbols.py +++ b/lib/esbonio/esbonio/server/features/sphinx_support/symbols.py @@ -9,13 +9,13 @@ from esbonio.server import EsbonioLanguageServer from esbonio.server import LanguageFeature from esbonio.server import Uri -from esbonio.server.features.sphinx_manager import SphinxManager +from esbonio.server.features.project_manager import ProjectManager class SphinxSymbols(LanguageFeature): """Add support for ``textDocument/documentSymbol`` requests""" - def __init__(self, server: EsbonioLanguageServer, manager: SphinxManager): + def __init__(self, server: EsbonioLanguageServer, manager: ProjectManager): super().__init__(server) self.manager = manager @@ -25,10 +25,10 @@ async def document_symbol( """Called when a document symbols request is received.""" uri = Uri.parse(params.text_document.uri) - if (client := await self.manager.get_client(uri)) is None: + if (project := self.manager.get_project(uri)) is None: return None - symbols = await client.get_document_symbols(uri) + symbols = await project.get_document_symbols(uri) if len(symbols) == 0: return None @@ -64,9 +64,9 @@ async def workspace_symbol( """Called when a workspace symbol request is received.""" tasks = [] - for client in self.manager.clients.values(): + for project in self.manager.projects.values(): tasks.append( - asyncio.create_task(client.get_workspace_symbols(params.query)) + asyncio.create_task(project.get_workspace_symbols(params.query)) ) symbols = await asyncio.gather(*tasks) @@ -94,6 +94,6 @@ async def workspace_symbol( return result -def esbonio_setup(server: EsbonioLanguageServer, sphinx_manager: SphinxManager): - symbols = SphinxSymbols(server, sphinx_manager) +def esbonio_setup(server: EsbonioLanguageServer, project_manager: ProjectManager): + symbols = SphinxSymbols(server, project_manager) server.add_feature(symbols) diff --git a/lib/esbonio/esbonio/server/server.py b/lib/esbonio/esbonio/server/server.py index 03d20ac94..254dfc0dc 100644 --- a/lib/esbonio/esbonio/server/server.py +++ b/lib/esbonio/esbonio/server/server.py @@ -4,6 +4,8 @@ import collections import inspect import logging +import platform +import traceback import typing from typing import TypeVar from uuid import uuid4 @@ -21,9 +23,11 @@ if typing.TYPE_CHECKING: from typing import Any from typing import Callable + from typing import Coroutine from typing import Dict from typing import List from typing import Optional + from typing import Set from typing import Tuple from typing import Type @@ -82,6 +86,9 @@ def __init__(self, logger: Optional[logging.Logger] = None, *args, **kwargs): self._ready: asyncio.Future[bool] = asyncio.Future() """Indicates if the server is ready.""" + self._tasks: Set[asyncio.Task] = set() + """Used to hold running tasks""" + self.logger = logger or logging.getLogger(__name__) """The logger instance to use.""" @@ -100,8 +107,34 @@ def converter(self) -> cattrs.Converter: """The cattrs converter instance we should use.""" return self.lsp._converter + def _finish_task(self, task: asyncio.Task[Any]): + """Cleanup a finished task.""" + self.logger.debug("Task finished: %s", task) + self._tasks.discard(task) + + if (exc := task.exception()) is not None: + self.logger.error( + "Error in async task\n%s", + traceback.format_exception(type(exc), exc, exc.__traceback__), + ) + + def run_task(self, coro: Coroutine, *, name: Optional[str] = None) -> asyncio.Task: + """Convert a given coroutine into a task and ensure it is executed.""" + + task = asyncio.create_task(coro, name=name) + self.logger.debug("Scheduled task: %s", task) + task.add_done_callback(self._finish_task) + + self._tasks.add(task) + return task + def initialize(self, params: types.InitializeParams): - self.logger.info("Initialising esbonio v%s", __version__) + self.logger.info( + "Initialising esbonio v%s, using Python v%s on %s", + __version__, + platform.python_version(), + platform.platform(aliased=True, terse=True), + ) if (client := params.client_info) is not None: self.logger.info("Language client: %s %s", client.name, client.version) @@ -115,14 +148,18 @@ def initialize(self, params: types.InitializeParams): self.configuration.initialization_options = params.initialization_options async def initialized(self, params: types.InitializedParams): + self.configuration.update_file_configuration() + await asyncio.gather( self.configuration.update_workspace_configuration(), - self.configuration.update_file_configuration(), self._register_did_change_configuration_handler(), self._register_did_change_watched_files_handler(), ) self._ready.set_result(True) + def lsp_shutdown(self, params: None): + """Called when the server is instructed to ``shutdown`` by the client.""" + def load_extension(self, name: str, setup: Callable): """Load the given setup function as an extension. diff --git a/lib/esbonio/esbonio/server/setup.py b/lib/esbonio/esbonio/server/setup.py index ff15e0987..b2ea77596 100644 --- a/lib/esbonio/esbonio/server/setup.py +++ b/lib/esbonio/esbonio/server/setup.py @@ -57,6 +57,11 @@ async def on_initialized( await ls.initialized(params) await call_features(ls, "initialized", params) + @server.feature(types.SHUTDOWN) + async def on_shutdown(ls: EsbonioLanguageServer, params: None): + ls.lsp_shutdown(params) + await call_features(ls, "shutdown", params) + @server.feature(types.TEXT_DOCUMENT_DID_CHANGE) async def on_document_change( ls: EsbonioLanguageServer, params: types.DidChangeTextDocumentParams diff --git a/lib/esbonio/esbonio/server/testing.py b/lib/esbonio/esbonio/server/testing.py index daccc9eae..1afd7a6aa 100644 --- a/lib/esbonio/esbonio/server/testing.py +++ b/lib/esbonio/esbonio/server/testing.py @@ -1,4 +1,5 @@ """Helpers and utilities for writing tests.""" + from lsprotocol import types diff --git a/lib/esbonio/esbonio/sphinx_agent/app.py b/lib/esbonio/esbonio/sphinx_agent/app.py index 8cad81d9f..90bfdff38 100644 --- a/lib/esbonio/esbonio/sphinx_agent/app.py +++ b/lib/esbonio/esbonio/sphinx_agent/app.py @@ -1,12 +1,30 @@ from __future__ import annotations +import logging import pathlib -import typing +from typing import IO from sphinx.application import Sphinx as _Sphinx +from sphinx.util import console +from sphinx.util import logging as sphinx_logging_module +from sphinx.util.logging import NAMESPACE as SPHINX_LOG_NAMESPACE from .database import Database -from .log import SphinxLogHandler +from .log import DiagnosticFilter + +sphinx_logger = logging.getLogger(SPHINX_LOG_NAMESPACE) +sphinx_log_setup = sphinx_logging_module.setup + + +def setup_logging(app: Sphinx, status: IO, warning: IO): + + # Run the usual setup + sphinx_log_setup(app, status, warning) + + # Attach our diagnostic filter to the warning handler. + for handler in sphinx_logger.handlers: + if handler.level == logging.WARNING: + handler.addFilter(app.esbonio.log) class Esbonio: @@ -14,11 +32,14 @@ class Esbonio: db: Database - log: SphinxLogHandler + log: DiagnosticFilter - def __init__(self, dbpath: pathlib.Path): + def __init__(self, dbpath: pathlib.Path, app: _Sphinx): self.db = Database(dbpath) - self.log = typing.cast(SphinxLogHandler, None) + self.log = DiagnosticFilter(app) + + # Override sphinx's usual logging setup function + sphinx_logging_module.setup = setup_logging # type: ignore class Sphinx(_Sphinx): @@ -27,7 +48,12 @@ class Sphinx(_Sphinx): esbonio: Esbonio def __init__(self, *args, **kwargs): - dbpath = pathlib.Path(kwargs["outdir"], "esbonio.db").resolve() - self.esbonio = Esbonio(dbpath) + # Disable color codes + console.nocolor() + + self.esbonio = Esbonio( + dbpath=pathlib.Path(kwargs["outdir"], "esbonio.db").resolve(), + app=self, + ) super().__init__(*args, **kwargs) diff --git a/lib/esbonio/esbonio/sphinx_agent/config.py b/lib/esbonio/esbonio/sphinx_agent/config.py index cdea2a13d..73507581a 100644 --- a/lib/esbonio/esbonio/sphinx_agent/config.py +++ b/lib/esbonio/esbonio/sphinx_agent/config.py @@ -1,6 +1,7 @@ import dataclasses import inspect import pathlib +import sys from typing import Any from typing import Dict from typing import List @@ -152,9 +153,9 @@ def to_application_args(self) -> Dict[str, Any]: "outdir": str(build_dir), "parallel": self.parallel, "srcdir": str(src_dir), - "status": None, + "status": sys.stderr, "tags": self.tags, "verbosity": self.verbosity, - "warning": None, + "warning": sys.stderr, "warningiserror": self.warning_is_error, } diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py b/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py index 63d8000b5..86086d0f2 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py @@ -5,28 +5,22 @@ import sys import traceback import typing -from functools import partial -from typing import IO from typing import Callable from typing import Dict from typing import List from typing import Optional from typing import Tuple from typing import Type -from uuid import uuid4 import sphinx.application from sphinx import __version__ as __sphinx_version__ -from sphinx.util import console -from sphinx.util import logging as sphinx_logging_module from sphinx.util.logging import NAMESPACE as SPHINX_LOG_NAMESPACE -from sphinx.util.logging import VERBOSITY_MAP from .. import types from ..app import Sphinx from ..config import SphinxConfig -from ..log import SphinxLogHandler from ..transforms import LineNumberTransform +from ..types import Uri from ..util import send_error from ..util import send_message @@ -49,7 +43,7 @@ def __init__(self): self.app: Optional[Sphinx] = None """The sphinx application instance""" - self._content_overrides: Dict[pathlib.Path, str] = {} + self._content_overrides: Dict[Uri, str] = {} """Holds any additional content to inject into a build.""" self._handlers: Dict[str, Tuple[Type, Callable]] = self._register_handlers() @@ -120,9 +114,6 @@ def create_sphinx_app(self, request: types.CreateApplicationRequest): raise ValueError("Invalid build command") sphinx_args = sphinx_config.to_application_args() - - # Override Sphinx's logging setup with our own. - sphinx_logging_module.setup = partial(self.setup_logging, sphinx_config) self.app = Sphinx(**sphinx_args) # Connect event handlers. @@ -138,7 +129,6 @@ def create_sphinx_app(self, request: types.CreateApplicationRequest): response = types.CreateApplicationResponse( id=request.id, result=types.SphinxInfo( - id=str(uuid4()), version=__sphinx_version__, conf_dir=str(self.app.confdir), build_dir=str(self.app.outdir), @@ -156,48 +146,17 @@ def _cb_env_before_read_docs(self, app: Sphinx, env, docnames: List[str]): is_building = set(docnames) for docname in env.found_docs - is_building: - filepath = pathlib.Path(env.doc2path(docname, base=True)) - if filepath in self._content_overrides: + uri = Uri.for_file(env.doc2path(docname, base=True)) + if uri in self._content_overrides: docnames.append(docname) def _cb_source_read(self, app: Sphinx, docname: str, source): """Called whenever sphinx reads a file from disk.""" - filepath = app.env.doc2path(docname, base=True) - - # Override file contents if necessary - path = pathlib.Path(filepath) - if (content := self._content_overrides.get(path)) is not None: + uri = Uri.for_file(app.env.doc2path(docname, base=True)) + if (content := self._content_overrides.get(uri, None)) is not None: source[0] = content - def setup_logging(self, config: SphinxConfig, app: Sphinx, status: IO, warning: IO): - """Setup Sphinx's logging so that it integrates well with the parent language - server.""" - - # Disable color escape codes in Sphinx's log messages - console.nocolor() - - if not config.silent: - # Be sure to remove any old handlers - for handler in sphinx_logger.handlers: - if isinstance(handler, SphinxLogHandler): - sphinx_logger.handlers.remove(handler) - self.log_handler = None - - app.esbonio.log = SphinxLogHandler(app) - sphinx_logger.addHandler(app.esbonio.log) - - if config.quiet: - level = logging.WARNING - else: - level = VERBOSITY_MAP[app.verbosity] - - sphinx_logger.setLevel(level) - app.esbonio.log.setLevel(level) - - formatter = logging.Formatter("%(message)s") - app.esbonio.log.setFormatter(formatter) - def build_sphinx_app(self, request: types.BuildRequest): """Trigger a Sphinx build.""" @@ -206,7 +165,7 @@ def build_sphinx_app(self, request: types.BuildRequest): return self._content_overrides = { - pathlib.Path(p): content + Uri.parse(p): content for p, content in request.params.content_overrides.items() } @@ -226,6 +185,9 @@ def build_sphinx_app(self, request: types.BuildRequest): id=request.id, code=-32603, message=f"sphinx-build failed: {message}" ) + finally: + self.app._warncount = 0 + def notify_exit(self, request: types.ExitNotification): """Sent from the client to signal that the agent should exit.""" sys.exit(0) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/diagnostics.py b/lib/esbonio/esbonio/sphinx_agent/handlers/diagnostics.py index 331dd2821..70c47d1b3 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/diagnostics.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/diagnostics.py @@ -22,8 +22,8 @@ def init_db(app: Sphinx, config: Config): def clear_diagnostics(app: Sphinx, docname: str, source): """Clear the diagnostics assocated with the given file.""" - filepath = app.env.doc2path(docname, base=True) - app.esbonio.log.diagnostics.pop(filepath, None) + uri = Uri.for_file(app.env.doc2path(docname, base=True)) + app.esbonio.log.diagnostics.pop(uri, None) def sync_diagnostics(app: Sphinx, exc: Optional[Exception]): @@ -32,10 +32,9 @@ def sync_diagnostics(app: Sphinx, exc: Optional[Exception]): results = [] diagnostics = app.esbonio.log.diagnostics - for fpath, items in diagnostics.items(): - uri = str(Uri.for_file(fpath).resolve()) + for uri, items in diagnostics.items(): for item in items: - results.append((uri, as_json(item))) + results.append((str(uri), as_json(item))) app.esbonio.db.insert_values(DIAGNOSTICS_TABLE, results) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/symbols.py b/lib/esbonio/esbonio/sphinx_agent/handlers/symbols.py index 2325539bd..4364686f1 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/symbols.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/symbols.py @@ -298,11 +298,9 @@ def depart_a_directive(self, node: nodes.Node): # TODO: Enable symbols for roles # However the reported line numbers can be inaccurate... - def visit_a_role(self, node: nodes.Node) -> None: - ... + def visit_a_role(self, node: nodes.Node) -> None: ... - def depart_a_role(self, node: nodes.Node) -> None: - ... + def depart_a_role(self, node: nodes.Node) -> None: ... # TODO: Enable symbols for definition list items # However the reported line numbers appear to be inaccurate... diff --git a/lib/esbonio/esbonio/sphinx_agent/log.py b/lib/esbonio/esbonio/sphinx_agent/log.py index 0d57056d8..4ec7b6320 100644 --- a/lib/esbonio/esbonio/sphinx_agent/log.py +++ b/lib/esbonio/esbonio/sphinx_agent/log.py @@ -1,24 +1,26 @@ +from __future__ import annotations + import inspect import logging import os import pathlib import sys -from types import ModuleType -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple -from typing import Union - -from sphinx.util.logging import OnceFilter -from sphinx.util.logging import SphinxLogRecord -from sphinx.util.logging import WarningLogRecordTranslator +import typing from . import types +from .types import Uri from .util import logger -from .util import send_message + +if typing.TYPE_CHECKING: + from types import ModuleType + from typing import Any + from typing import Dict + from typing import List + from typing import Optional + from typing import Set + from typing import Tuple + from typing import Union + DIAGNOSTIC_SEVERITY = { logging.ERROR: types.DiagnosticSeverity.Error, @@ -27,16 +29,14 @@ } -class SphinxLogHandler(logging.Handler): +class DiagnosticFilter(logging.Filter): """A logging handler that can extract errors from Sphinx's build output.""" def __init__(self, app, *args, **kwargs): super().__init__(*args, **kwargs) self.app = app - self.translator = WarningLogRecordTranslator(app) - self.only_once = OnceFilter() - self.diagnostics: Dict[str, Set[types.Diagnostic]] = {} + self.diagnostics: Dict[Uri, Set[types.Diagnostic]] = {} def get_location(self, location: str) -> Tuple[str, Optional[int]]: if not location: @@ -105,27 +105,16 @@ def get_docstring_location(self, target: str, offset: str) -> Optional[int]: logger.debug("Unable to determine diagnostic location\n%s", exc_info=True) return None - def emit(self, record: logging.LogRecord) -> None: + def filter(self, record: logging.LogRecord) -> bool: conditions = [ "sphinx" not in record.name, record.levelno not in {logging.WARNING, logging.ERROR}, ] if any(conditions): - # Log the record as normal - self.do_emit(record) - return + return True - # Let sphinx extract location info for warning/error messages - self.translator.filter(record) # type: ignore - - # Only process errors/warnings once. - # Note: This isn't a silver bullet as it only catches messages that are explicitly - # marked as to be logged only once e.g. logger.warning(..., once=True). - if not self.only_once.filter(record): - return - - loc = record.location if isinstance(record, SphinxLogRecord) else "" + loc = getattr(record, "location", "") doc, lineno = self.get_location(loc) line = lineno or 1 @@ -155,9 +144,5 @@ def emit(self, record: logging.LogRecord) -> None: ), ) - self.diagnostics.setdefault(doc, set()).add(diagnostic) - self.do_emit(record) - - def do_emit(self, record): - params = types.LogMessageParams(message=self.format(record).strip(), type=4) - send_message(types.LogMessage(params=params)) + self.diagnostics.setdefault(Uri.for_file(doc), set()).add(diagnostic) + return True diff --git a/lib/esbonio/esbonio/sphinx_agent/types.py b/lib/esbonio/esbonio/sphinx_agent/types.py index a7dc94391..89b17c63a 100644 --- a/lib/esbonio/esbonio/sphinx_agent/types.py +++ b/lib/esbonio/esbonio/sphinx_agent/types.py @@ -3,6 +3,7 @@ This is the *only* file shared between the agent itself and the parent language server. For this reason this file *cannot* import anything from Sphinx. """ + import dataclasses import enum import os @@ -215,6 +216,41 @@ def __post_init__(self): "Paths without an authority cannot start with two slashes '//'" ) + def __eq__(self, other): + if type(other) is not type(self): + return False + + if self.scheme != other.scheme: + return False + + if self.authority != other.authority: + return False + + if self.query != other.query: + return False + + if self.fragment != other.fragment: + return False + + if IS_WIN and self.scheme == "file": + # Filepaths on windows are case in-sensitive + if self.path.lower() != other.path.lower(): + return False + + elif self.path != other.path: + return False + + return True + + def __hash__(self): + if IS_WIN and self.scheme == "file": + # Filepaths on windows are case in-sensitive + path = self.path.lower() + else: + path = self.path + + return hash((self.scheme, self.authority, path, self.query, self.fragment)) + def __fspath__(self): """Return the file system representation of this uri. @@ -533,9 +569,6 @@ class CreateApplicationRequest: class SphinxInfo: """Represents information about an instance of the Sphinx application.""" - id: str - """A unique id representing a particular Sphinx application instance.""" - version: str """The version of Sphinx being used.""" diff --git a/lib/esbonio/pyproject.toml b/lib/esbonio/pyproject.toml index 510126a2b..2ba0f831f 100644 --- a/lib/esbonio/pyproject.toml +++ b/lib/esbonio/pyproject.toml @@ -40,7 +40,6 @@ dependencies = [ [project.scripts] esbonio = "esbonio.server.cli:main" -esbonio-sphinx = "esbonio.lsp.sphinx.cli:main" [project.optional-dependencies] typecheck = ["mypy", "pytest-lsp>=0.3.1", "types-docutils", "types-pygments"] @@ -67,16 +66,19 @@ profile = "black" [tool.pytest.ini_options] addopts = "--doctest-glob='*.txt'" asyncio_mode = "auto" -filterwarnings = [ - "ignore:'contextfunction' is renamed to 'pass_context',*:DeprecationWarning", - "ignore:'environmentfilter' is renamed to 'pass_environment',*:DeprecationWarning", -] [tool.mypy] mypy_path = "$MYPY_CONFIG_FILE_DIR" explicit_package_bases = true check_untyped_defs = true +[tool.pyright] +venv = ".env" +include = ["esbonio"] + +pythonVersion = "3.8" +pythonPlatform = "All" + [tool.towncrier] filename = "CHANGES.md" directory = "changes/" diff --git a/lib/esbonio/pyrightconfig.json b/lib/esbonio/pyrightconfig.json deleted file mode 100644 index 0622ac86c..000000000 --- a/lib/esbonio/pyrightconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "venv": ".env" -} diff --git a/lib/esbonio/setup.cfg b/lib/esbonio/setup.cfg index 3e6eb72e3..d13cdbaad 100644 --- a/lib/esbonio/setup.cfg +++ b/lib/esbonio/setup.cfg @@ -1,3 +1,3 @@ [flake8] max-line-length = 88 -ignore = E501,W503,E402,E203 +ignore = E501,W503,E402,E203,E701,E704 diff --git a/lib/esbonio/tests/e2e/conftest.py b/lib/esbonio/tests/e2e/conftest.py index b6681ac0d..38ab016ac 100644 --- a/lib/esbonio/tests/e2e/conftest.py +++ b/lib/esbonio/tests/e2e/conftest.py @@ -37,7 +37,7 @@ async def client(lsp_client: LanguageClient, uri_for, tmp_path_factory): ), ), initialization_options={ - "server": {"logLevel": "debug"}, + "logging": {"level": "debug"}, "sphinx": { "buildCommand": [ "sphinx-build", diff --git a/lib/esbonio/tests/e2e/test_e2e_diagnostics.py b/lib/esbonio/tests/e2e/test_e2e_diagnostics.py index dd6209c30..885d81df7 100644 --- a/lib/esbonio/tests/e2e/test_e2e_diagnostics.py +++ b/lib/esbonio/tests/e2e/test_e2e_diagnostics.py @@ -11,13 +11,12 @@ TEST_DIR = pathlib.Path(__file__).parent.parent -@pytest.mark.asyncio -@pytest.mark.skip -async def test_document_diagnostic(pull_client: LanguageClient, uri_for): - """Ensure that we can get the diagnostics for a single document correctly.""" +@pytest.mark.asyncio(scope="session") +async def test_rst_document_diagnostic(client: LanguageClient, uri_for): + """Ensure that we can get the diagnostics for a single rst document correctly.""" - workspace_uri = uri_for("sphinx-default", "workspace") - test_uri = workspace_uri / "definitions.rst" + workspace_uri = uri_for("workspaces", "demo") + test_uri = workspace_uri / "rst" / "diagnostics.rst" report = await client.text_document_diagnostic_async( types.DocumentDiagnosticParams( text_document=types.TextDocumentIdentifier(uri=str(test_uri)) @@ -30,30 +29,50 @@ async def test_document_diagnostic(pull_client: LanguageClient, uri_for): # test cases. messages = {d.message for d in report.items} assert messages == { - "image file not readable: _static/bad.png", - "unknown document: '/changelog'", + "image file not readable: not-an-image.png", } assert len(client.diagnostics) == 0, "Server should not publish diagnostics" -@pytest.mark.asyncio -@pytest.mark.skip -async def test_workspace_diagnostic(pull_client: LanguageClient, uri_for): +@pytest.mark.asyncio(scope="session") +async def test_myst_document_diagnostic(client: LanguageClient, uri_for): + """Ensure that we can get the diagnostics for a single myst document correctly.""" + + workspace_uri = uri_for("workspaces", "demo") + test_uri = workspace_uri / "myst" / "diagnostics.md" + report = await client.text_document_diagnostic_async( + types.DocumentDiagnosticParams( + text_document=types.TextDocumentIdentifier(uri=str(test_uri)) + ) + ) + + assert report.kind == "full" + + # We will only check the diagnostic message, full details will be handled by other + # test cases. + messages = {d.message for d in report.items} + assert messages == { + "image file not readable: not-an-image.png", + } + + assert len(client.diagnostics) == 0, "Server should not publish diagnostics" + + +@pytest.mark.asyncio(scope="session") +async def test_workspace_diagnostic(client: LanguageClient, uri_for): """Ensure that we can get diagnostics for the whole workspace correctly.""" report = await client.workspace_diagnostic_async( types.WorkspaceDiagnosticParams(previous_result_ids=[]) ) - workspace_uri = uri_for("sphinx-default", "workspace") + workspace_uri = uri_for("workspaces", "demo") expected = { - str(workspace_uri / "definitions.rst"): { - "image file not readable: _static/bad.png", - "unknown document: '/changelog'", + str(workspace_uri / "rst" / "diagnostics.rst"): { + "image file not readable: not-an-image.png", }, - str(workspace_uri / "directive_options.rst"): { - "image file not readable: filename.png", - "document isn't included in any toctree", + str(workspace_uri / "myst" / "diagnostics.md"): { + "image file not readable: not-an-image.png", }, } assert len(report.items) == len(expected) @@ -71,9 +90,10 @@ async def test_workspace_diagnostic(pull_client: LanguageClient, uri_for): ) async def pub_client(lsp_client: LanguageClient, uri_for, tmp_path_factory): """A client that does **not** support the pull-diagnostics model.""" + build_dir = tmp_path_factory.mktemp("build") - workspace_uri = uri_for("sphinx-default", "workspace") - test_uri = workspace_uri / "definitions.rst" + workspace_uri = uri_for("workspaces", "demo") + test_uri = workspace_uri / "rst" / "diagnostics.rst" await lsp_client.initialize_session( types.InitializeParams( @@ -84,7 +104,7 @@ async def pub_client(lsp_client: LanguageClient, uri_for, tmp_path_factory): ), ), initialization_options={ - "server": {"logLevel": "debug"}, + "logging": {"level": "debug"}, "sphinx": { "buildCommand": [ "sphinx-build", @@ -97,7 +117,7 @@ async def pub_client(lsp_client: LanguageClient, uri_for, tmp_path_factory): }, }, workspace_folders=[ - types.WorkspaceFolder(uri=str(workspace_uri), name="sphinx-default"), + types.WorkspaceFolder(uri=str(workspace_uri), name="demo"), ], ) ) @@ -135,19 +155,16 @@ async def pub_client(lsp_client: LanguageClient, uri_for, tmp_path_factory): await lsp_client.shutdown_session() -@pytest.mark.asyncio -@pytest.mark.skip +@pytest.mark.asyncio(scope="module") async def test_publish_diagnostics(pub_client: LanguageClient, uri_for): """Ensure that the server publishes the diagnostics it finds""" - workspace_uri = uri_for("sphinx-default", "workspace") + workspace_uri = uri_for("workspaces", "demo") expected = { - str(workspace_uri / "definitions.rst"): { - "image file not readable: _static/bad.png", - "unknown document: '/changelog'", + str(workspace_uri / "rst" / "diagnostics.rst"): { + "image file not readable: not-an-image.png", }, - str(workspace_uri / "directive_options.rst"): { - "image file not readable: filename.png", - "document isn't included in any toctree", + str(workspace_uri / "myst" / "diagnostics.md"): { + "image file not readable: not-an-image.png", }, } diff --git a/lib/esbonio/tests/e2e/test_sphinx_manager.py b/lib/esbonio/tests/e2e/test_sphinx_manager.py new file mode 100644 index 000000000..c5c52c7f9 --- /dev/null +++ b/lib/esbonio/tests/e2e/test_sphinx_manager.py @@ -0,0 +1,342 @@ +"""Technically, not a true end-to-end test as we're inspecting the internals of the server. +But since it depends on the sphinx agent, we may as well put it here.""" + +from __future__ import annotations + +import asyncio +import io +import pathlib +import sys +import typing + +import pytest +import pytest_asyncio +from lsprotocol import types as lsp +from pygls.server import StdOutTransportAdapter + +from esbonio.server import EsbonioLanguageServer +from esbonio.server import Uri +from esbonio.server import create_language_server +from esbonio.server.features.project_manager import ProjectManager +from esbonio.server.features.sphinx_manager import ClientState +from esbonio.server.features.sphinx_manager import SphinxManager +from esbonio.server.features.sphinx_manager import make_subprocess_sphinx_client + +if typing.TYPE_CHECKING: + from typing import Any + from typing import Callable + from typing import Tuple + + ServerManager = Callable[[Any], Tuple[EsbonioLanguageServer, SphinxManager]] + + +@pytest.fixture() +def demo_workspace(uri_for): + return uri_for("workspaces", "demo") + + +@pytest.fixture() +def docs_workspace(uri_for): + return uri_for("..", "..", "..", "docs") + + +@pytest_asyncio.fixture() +async def server_manager(demo_workspace: Uri, docs_workspace): + """An instance of the language server and sphinx manager to use for the test.""" + + loop = asyncio.get_running_loop() + + esbonio = create_language_server(EsbonioLanguageServer, [], loop=loop) + esbonio.lsp.transport = StdOutTransportAdapter(io.BytesIO(), sys.stderr.buffer) + + project_manager = ProjectManager(esbonio) + esbonio.add_feature(project_manager) + + sphinx_manager = SphinxManager( + make_subprocess_sphinx_client, project_manager, esbonio + ) + esbonio.add_feature(sphinx_manager) + + def initialize(init_options): + # Initialize the server. + esbonio.lsp._procedure_handler( + lsp.InitializeRequest( + id=1, + params=lsp.InitializeParams( + capabilities=lsp.ClientCapabilities(), + initialization_options=init_options, + workspace_folders=[ + lsp.WorkspaceFolder(uri=str(demo_workspace), name="demo"), + lsp.WorkspaceFolder(uri=str(docs_workspace), name="docs"), + ], + ), + ) + ) + + esbonio.lsp._procedure_handler( + lsp.InitializedNotification(params=lsp.InitializedParams()) + ) + return esbonio, sphinx_manager + + yield initialize + + await sphinx_manager.shutdown(None) + + +@pytest.mark.asyncio +async def test_get_client( + server_manager: ServerManager, demo_workspace: Uri, tmp_path: pathlib.Path +): + """Ensure that we can create a SphinxClient correctly.""" + + server, manager = server_manager( + dict( + esbonio=dict( + sphinx=dict( + pythonCommand=[sys.executable], + buildCommand=["sphinx-build", "-M", "dirhtml", ".", str(tmp_path)], + ), + ), + ), + ) + # Ensure that the server is ready + await server.ready + + result = await manager.get_client(demo_workspace / "index.rst") + # At least for now, the first call to get_client will not return a client + # but will instead "prime the system" ready to return a client the next + # time it is called. + assert result is None + + # Give the async tasks chance to complete. + await asyncio.sleep(0.5) + + # A client should have been created and started + client = manager.clients[str(demo_workspace)] + + assert client is not None + assert client.state in {ClientState.Starting, ClientState.Running} + + # Now when we ask for the client, the client should be started and we should + # get back the same instance + result = await manager.get_client(demo_workspace / "index.rst") + + assert result is client + assert client.state == ClientState.Running + + # And that the client initialized correctly + assert client.builder == "dirhtml" + assert client.src_uri == demo_workspace + assert client.conf_uri == demo_workspace + assert client.build_uri == Uri.for_file(tmp_path / "dirhtml") + + +@pytest.mark.asyncio +async def test_get_client_with_error( + server_manager: ServerManager, demo_workspace: Uri +): + """Ensure that we correctly handle the case where there is an error with a client.""" + + server, manager = server_manager(None) + # Ensure that the server is ready + await server.ready + + result = await manager.get_client(demo_workspace / "index.rst") + # At least for now, the first call to get_client will not return a client + # but will instead "prime the system" ready to return a client the next + # time it is called. + assert result is None + + # Give the async tasks chance to complete. + await asyncio.sleep(0.5) + + # A client should have been created and started + client = manager.clients[str(demo_workspace)] + + assert client is not None + assert client.state in {ClientState.Starting, ClientState.Errored} + + # Now when we ask for the client, the client should be started and we should + # get back the same instance + result = await manager.get_client(demo_workspace / "index.rst") + + assert result is client + assert client.state == ClientState.Errored + assert "No python environment configured" in str(client.exception) + + # Finally, if we request another uri from the same project we should get back + # the same client instance - even though it failed to start. + result = await manager.get_client(demo_workspace / "demo_myst.md") + assert result is client + + +@pytest.mark.asyncio +async def test_get_client_with_many_uris( + server_manager: ServerManager, demo_workspace: Uri, tmp_path: pathlib.Path +): + """Ensure that when called in rapid succession, with many uris we only create a + single client instance.""" + + server, manager = server_manager( + dict( + esbonio=dict( + sphinx=dict( + pythonCommand=[sys.executable], + buildCommand=["sphinx-build", "-M", "dirhtml", ".", str(tmp_path)], + ), + ), + ), + ) + + # Ensure that the server is ready + await server.ready + + src_uris = [Uri.for_file(f) for f in pathlib.Path(demo_workspace).glob("**/*.rst")] + coros = [manager.get_client(s) for s in src_uris] + + # As with the other cases, this should only "prime" the system + result = await asyncio.gather(*coros) + assert all([r is None for r in result]) + + # There should only have been one client created (in addition to the 'dummy' global + # scoped client) + assert len(manager.clients) == 2 + + client = manager.clients[str(demo_workspace)] + assert client is not None + assert client.state is None + + # Now if we do the same again we should get the same client instance for each case. + coros = [manager.get_client(s) for s in src_uris] + result = await asyncio.gather(*coros) + + assert all([r is client for r in result]) + + client = result[0] + assert client.state == ClientState.Running + + # And that the client initialized correctly + assert client.builder == "dirhtml" + assert client.src_uri == demo_workspace + assert client.conf_uri == demo_workspace + assert client.build_uri == Uri.for_file(tmp_path / "dirhtml") + + +@pytest.mark.asyncio +async def test_get_client_with_many_uris_in_many_projects( + server_manager: ServerManager, + demo_workspace: Uri, + docs_workspace: Uri, + tmp_path: pathlib.Path, +): + """Ensure that when called in rapid succession, with many uris we only create a + single client instance for each project.""" + + server, manager = server_manager( + dict( + esbonio=dict( + sphinx=dict(pythonCommand=[sys.executable]), + buildCommand=["sphinx-build", "-M", "dirhtml", ".", str(tmp_path)], + ), + ), + ) # Ensure that the server is ready + await server.ready + + src_uris = [Uri.for_file(f) for f in pathlib.Path(demo_workspace).glob("**/*.rst")] + src_uris += [Uri.for_file(f) for f in pathlib.Path(docs_workspace).glob("**/*.rst")] + coros = [manager.get_client(s) for s in src_uris] + + # As with the other cases, this should only "prime" the system + result = await asyncio.gather(*coros) + assert all([r is None for r in result]) + + # There should only have been one client created for each project (in addition to + # the 'dummy' global scoped client) + assert len(manager.clients) == 3 + + demo_client = manager.clients[str(demo_workspace)] + assert demo_client is not None + assert demo_client.state is None + + docs_client = manager.clients[str(docs_workspace)] + assert docs_client is not None + assert docs_client.state is None + + # Now if we do the same again we should get the same client instance for each case. + coros = [manager.get_client(s) for s in src_uris] + result = await asyncio.gather(*coros) + + assert all([(r is demo_client) or (r is docs_client) for r in result]) + + assert demo_client.state == ClientState.Running + + # When run in CI, the docs won't have all the required dependencies available. + assert docs_client.state in {ClientState.Running, ClientState.Errored} + + +@pytest.mark.asyncio +async def test_updated_config( + server_manager: ServerManager, demo_workspace: Uri, tmp_path: pathlib.Path +): + """Ensure that when the configuration affecting a Sphinx configuration is changed, + the SphinxClient is recreated.""" + + server, manager = server_manager( + dict( + esbonio=dict( + sphinx=dict( + pythonCommand=[sys.executable], + buildCommand=["sphinx-build", "-M", "dirhtml", ".", str(tmp_path)], + ), + ), + ), + ) + # Ensure that the server is ready + await server.ready + + result = await manager.get_client(demo_workspace / "index.rst") + # At least for now, the first call to get_client will not return a client + # but will instead "prime the system" ready to return a client the next + # time it is called. + assert result is None + + # Give the async tasks chance to complete. + await asyncio.sleep(0.5) + + # A client should have been created and started + client = manager.clients[str(demo_workspace)] + + assert client is not None + assert client.state in {ClientState.Starting, ClientState.Running} + + # Now when we ask for the client, the client should be started and we should + # get back the same instance + result = await manager.get_client(demo_workspace / "index.rst") + + assert result is client + assert client.state == ClientState.Running + + # And that the client initialized correctly + assert client.builder == "dirhtml" + + # Now update the configuration + server.configuration._initialization_options["esbonio"]["sphinx"][ + "buildCommand" + ] = ["sphinx-build", "-M", "html", ".", str(tmp_path)] + server.configuration._notify_subscriptions() + + # Give the async tasks chance to complete. + await asyncio.sleep(0.5) + + # A new client should have been created, started and be using the new config + new_client = manager.clients[str(demo_workspace)] + + assert new_client is not client + assert new_client.state in {ClientState.Starting, ClientState.Running} + + # Ensure that the client has finished starting + await new_client + assert new_client.builder == "html" + + # The old client should have been stopped + assert client.state == ClientState.Exited diff --git a/lib/esbonio/tests/server/features/test_logging.py b/lib/esbonio/tests/server/features/test_logging.py new file mode 100644 index 000000000..2f77d7238 --- /dev/null +++ b/lib/esbonio/tests/server/features/test_logging.py @@ -0,0 +1,311 @@ +from __future__ import annotations + +import typing + +import pytest + +from esbonio import server +from esbonio.server.features.log import LoggerConfiguration +from esbonio.server.features.log import LoggingConfig + +if typing.TYPE_CHECKING: + from typing import Dict + + +SERVER = server.EsbonioLanguageServer + + +@pytest.mark.parametrize( + "config,expected", + [ + ( # Check the defaults + LoggingConfig(), + dict( + version=1, + disable_existing_loggers=True, + formatters=dict( + fmt01=dict(format="[%(name)s] %(message)s"), + fmt02=dict(format="%(message)s"), + ), + handlers=dict( + stderr01={ + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "fmt01", + "stream": "ext://sys.stderr", + }, + stderr02={ + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "fmt02", + "stream": "ext://sys.stderr", + }, + ), + loggers=dict( + esbonio=dict( + level="ERROR", + propagate=False, + handlers=["stderr01"], + ), + sphinx=dict( + level="INFO", + propagate=False, + handlers=["stderr02"], + ), + ), + ), + ), + ( # It should be possible to override the base logging level + LoggingConfig(level="debug"), + dict( + version=1, + disable_existing_loggers=True, + formatters=dict( + fmt01=dict(format="[%(name)s] %(message)s"), + fmt02=dict(format="%(message)s"), + ), + handlers=dict( + stderr01={ + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "fmt01", + "stream": "ext://sys.stderr", + }, + stderr02={ + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "fmt02", + "stream": "ext://sys.stderr", + }, + ), + loggers=dict( + esbonio=dict( + level="DEBUG", + propagate=False, + handlers=["stderr01"], + ), + sphinx=dict( + level="INFO", + propagate=False, + handlers=["stderr02"], + ), + ), + ), + ), + ( # It should be possible to override the base log format + LoggingConfig(format="%(message)s"), + dict( + version=1, + disable_existing_loggers=True, + formatters=dict( + fmt01=dict(format="%(message)s"), + ), + handlers=dict( + stderr01={ + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "fmt01", + "stream": "ext://sys.stderr", + }, + ), + loggers=dict( + esbonio=dict( + level="ERROR", + propagate=False, + handlers=["stderr01"], + ), + sphinx=dict( + level="INFO", + propagate=False, + handlers=["stderr01"], + ), + ), + ), + ), + ( # User should be able to re-direct output to ``window/logMessages`` + LoggingConfig(stderr=False, window=True), + dict( + version=1, + disable_existing_loggers=True, + formatters=dict( + fmt01=dict(format="[%(name)s] %(message)s"), + fmt02=dict(format="%(message)s"), + ), + handlers=dict( + window01={ + "()": "esbonio.server.features.log.WindowLogMessageHandler", + "level": "DEBUG", + "formatter": "fmt01", + "server": SERVER, + }, + window02={ + "()": "esbonio.server.features.log.WindowLogMessageHandler", + "level": "DEBUG", + "formatter": "fmt02", + "server": SERVER, + }, + ), + loggers=dict( + esbonio=dict( + level="ERROR", + propagate=False, + handlers=["window01"], + ), + sphinx=dict( + level="INFO", + propagate=False, + handlers=["window02"], + ), + ), + ), + ), + ( # User should be able to re-direct output to a file + LoggingConfig(stderr=False, filepath="esbonio.log"), + dict( + version=1, + disable_existing_loggers=True, + formatters=dict( + fmt01=dict(format="[%(name)s] %(message)s"), + fmt02=dict(format="%(message)s"), + ), + handlers=dict( + file01={ + "class": "logging.FileHandler", + "level": "DEBUG", + "formatter": "fmt01", + "filename": "esbonio.log", + }, + file02={ + "class": "logging.FileHandler", + "level": "DEBUG", + "formatter": "fmt02", + "filename": "esbonio.log", + }, + ), + loggers=dict( + esbonio=dict( + level="ERROR", + propagate=False, + handlers=["file01"], + ), + sphinx=dict( + level="INFO", + propagate=False, + handlers=["file02"], + ), + ), + ), + ), + ( # User should be able to log to everything if they so desired + LoggingConfig(stderr=True, window=True, filepath="esbonio.log"), + dict( + version=1, + disable_existing_loggers=True, + formatters=dict( + fmt01=dict(format="[%(name)s] %(message)s"), + fmt02=dict(format="%(message)s"), + ), + handlers=dict( + file01={ + "class": "logging.FileHandler", + "level": "DEBUG", + "formatter": "fmt01", + "filename": "esbonio.log", + }, + stderr02={ + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "fmt01", + "stream": "ext://sys.stderr", + }, + window03={ + "()": "esbonio.server.features.log.WindowLogMessageHandler", + "level": "DEBUG", + "formatter": "fmt01", + "server": SERVER, + }, + file04={ + "class": "logging.FileHandler", + "level": "DEBUG", + "formatter": "fmt02", + "filename": "esbonio.log", + }, + stderr05={ + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "fmt02", + "stream": "ext://sys.stderr", + }, + window06={ + "()": "esbonio.server.features.log.WindowLogMessageHandler", + "level": "DEBUG", + "formatter": "fmt02", + "server": SERVER, + }, + ), + loggers=dict( + esbonio=dict( + level="ERROR", + propagate=False, + handlers=["file01", "stderr02", "window03"], + ), + sphinx=dict( + level="INFO", + propagate=False, + handlers=["file04", "stderr05", "window06"], + ), + ), + ), + ), + ( # The user should be able to customize an individual logger. + LoggingConfig( + config={"esbonio.Configuration": LoggerConfiguration(level="info")}, + ), + dict( + version=1, + disable_existing_loggers=True, + formatters=dict( + fmt01=dict(format="[%(name)s] %(message)s"), + fmt02=dict(format="%(message)s"), + ), + handlers=dict( + stderr01={ + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "fmt01", + "stream": "ext://sys.stderr", + }, + stderr02={ + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "fmt02", + "stream": "ext://sys.stderr", + }, + ), + loggers={ + "esbonio": dict( + level="ERROR", + propagate=False, + handlers=["stderr01"], + ), + "esbonio.Configuration": dict( + level="INFO", + propagate=False, + handlers=["stderr01"], + ), + "sphinx": dict( + level="INFO", + propagate=False, + handlers=["stderr02"], + ), + }, + ), + ), + ], +) +def test_logging_config(config: LoggingConfig, expected: Dict): + """Ensure that we can convert the user's config into the config we can pass to the + ``logging.config`` module correctly.""" + + # SERVER in this case is a class, at runtime this will be an actual instance. + assert config.to_logging_config(SERVER) == expected diff --git a/lib/esbonio/tests/server/features/test_sphinx_config.py b/lib/esbonio/tests/server/features/test_sphinx_config.py new file mode 100644 index 000000000..918ac9a64 --- /dev/null +++ b/lib/esbonio/tests/server/features/test_sphinx_config.py @@ -0,0 +1,178 @@ +import logging +import os +import pathlib +from typing import Optional + +import pytest +from lsprotocol.types import WorkspaceFolder +from pygls.workspace import Workspace + +from esbonio.server import Uri +from esbonio.server.features.sphinx_manager import SphinxConfig + +logger = logging.getLogger(__name__) + + +# Default values +CWD = os.path.join(".", "path", "to", "workspace")[1:] +PYTHON_CMD = ["/bin/python"] +BUILD_CMD = ["sphinx-build", "-M", "html", "src", "dest"] +PYPATH = [pathlib.Path("/path/to/site-packages/esbonio")] + + +def mk_uri(path: str) -> str: + return str(Uri.for_file(path)) + + +@pytest.mark.parametrize( + "uri, workspace, config, expected", + [ + ( # If everything is specified, resolve should be a no-op + "file:///path/to/workspace/file.rst", + Workspace(None), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + cwd=CWD, + python_path=PYPATH, + ), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + cwd=CWD, + python_path=PYPATH, + ), + ), + ( # If no cwd is given, and there is no available workspace root the config + # should be considered invalid + "file:///path/to/file.rst", + Workspace(None), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + ), + None, + ), + ( # If the workspace is empty, we should still be able to progress as long as + # the user provides a cwd + "file:///path/to/file.rst", + Workspace(None), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + cwd=CWD, + ), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + cwd=CWD, + ), + ), + ( # If only a ``root_uri`` is given use that. + "file:///path/to/workspace/file.rst", + Workspace(mk_uri(CWD)), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + ), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + cwd=CWD, + ), + ), + ( # Assuming that the requested uri resides within it. + "file:///path/to/other/workspace/file.rst", + Workspace(mk_uri(CWD)), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + ), + None, + ), + ( # Otherwise, prefer workspace_folders. + "file:///path/to/workspace/file.rst", + Workspace( + "file:///path/to", + workspace_folders=[WorkspaceFolder(mk_uri(CWD), "workspace")], + ), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + ), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + cwd=CWD, + ), + ), + ( # Handle multi-root scenarios. + "file:///path/to/workspace-b/file.rst", + Workspace( + "file:///path/to", + workspace_folders=[ + WorkspaceFolder("file:///path/to/workspace-a", "workspace-a"), + WorkspaceFolder("file:///path/to/workspace-b", "workspace-b"), + ], + ), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + ), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + cwd=os.path.join(".", "path", "to", "workspace-b")[1:], + ), + ), + ( # Again, make sure the requested uri resides within the workspace. + "file:///path/for/workspace-c/file.rst", + Workspace( + "file:///path/to", + workspace_folders=[ + WorkspaceFolder("file:///path/to/workspace-a", "workspace-a"), + WorkspaceFolder("file:///path/to/workspace-b", "workspace-b"), + ], + ), + SphinxConfig( + python_command=PYTHON_CMD, + build_command=BUILD_CMD, + python_path=PYPATH, + ), + None, + ), + ], +) +def test_resolve( + uri: str, + workspace: Workspace, + config: SphinxConfig, + expected: Optional[SphinxConfig], +): + """Ensure that we can resolve a user's configuration correctly. + + Parameters + ---------- + uri + The uri the config should be resolved relative to + + workspace + The workspace in which to resolve the configuration + + config + The base configuration to resolve + + expected + The expected outcome + """ + assert config.resolve(Uri.parse(uri), workspace, logger) == expected diff --git a/lib/esbonio/tests/server/features/test_sphinx_manager.py b/lib/esbonio/tests/server/features/test_sphinx_manager.py deleted file mode 100644 index 6067c0633..000000000 --- a/lib/esbonio/tests/server/features/test_sphinx_manager.py +++ /dev/null @@ -1,97 +0,0 @@ -import asyncio -from unittest.mock import AsyncMock -from unittest.mock import Mock - -import pytest -from lsprotocol import types as lsp -from pygls.exceptions import JsonRpcInternalError - -from esbonio.server import EsbonioLanguageServer -from esbonio.server import EsbonioWorkspace -from esbonio.server import Uri -from esbonio.server.features.sphinx_manager import MockSphinxClient -from esbonio.server.features.sphinx_manager import SphinxConfig -from esbonio.server.features.sphinx_manager import SphinxManager -from esbonio.server.features.sphinx_manager import mock_sphinx_client_factory -from esbonio.sphinx_agent import types - - -@pytest.fixture() -def workspace(uri_for): - return uri_for("workspaces", "demo") - - -@pytest.fixture() -def progress(): - p = Mock() - p.create_async = AsyncMock() - return p - - -@pytest.fixture() -def server(event_loop, progress, workspace: Uri): - """A mock instance of the language""" - ready = asyncio.Future() - ready.set_result(True) - - esbonio = EsbonioLanguageServer(loop=event_loop) - esbonio._ready = ready - esbonio.lsp.progress = progress - esbonio.show_message = Mock() - esbonio.configuration.get = Mock(return_value=SphinxConfig()) - esbonio.lsp._workspace = EsbonioWorkspace( - None, - workspace_folders=[lsp.WorkspaceFolder(str(workspace), "workspace")], - ) - return esbonio - - -@pytest.fixture() -def sphinx_info(workspace: Uri): - return types.SphinxInfo( - id="123", - version="6.0", - conf_dir=workspace.fs_path, - build_dir=(workspace / "_build" / "html").fs_path, - builder_name="html", - src_dir=workspace.fs_path, - dbpath=(workspace / "_build" / "html" / "esbonio.db").fs_path, - ) - - -@pytest.mark.asyncio -async def test_create_application_error(server, workspace: Uri): - """Ensure that we can handle errors during application creation correctly.""" - - client = MockSphinxClient(JsonRpcInternalError("create sphinx application failed.")) - client_factory = mock_sphinx_client_factory(client) - - manager = SphinxManager(client_factory, server) - - result = await manager.get_client(workspace / "index.rst") - assert result is None - - server.show_message.assert_called_with( - "Unable to create sphinx application: create sphinx application failed.", - lsp.MessageType.Error, - ) - - -@pytest.mark.asyncio -async def test_trigger_build_error(sphinx_info, server, workspace): - """Ensure that we can handle build errors correctly.""" - - client = MockSphinxClient( - sphinx_info, - build_result=JsonRpcInternalError("sphinx-build failed:"), - ) - client_factory = mock_sphinx_client_factory() - - manager = SphinxManager(client_factory, server) - manager.clients = {workspace: client} - - await manager.trigger_build(workspace / "index.rst") - - server.show_message.assert_called_with( - "sphinx-build failed:", lsp.MessageType.Error - ) diff --git a/lib/esbonio/tests/sphinx-agent/conftest.py b/lib/esbonio/tests/sphinx-agent/conftest.py index 9a65a990f..e98e86296 100644 --- a/lib/esbonio/tests/sphinx-agent/conftest.py +++ b/lib/esbonio/tests/sphinx-agent/conftest.py @@ -1,10 +1,12 @@ +import logging import sys -import pytest_lsp +import pytest_asyncio from lsprotocol.types import WorkspaceFolder from pygls.workspace import Workspace -from pytest_lsp import ClientServerConfig +from esbonio.server.features.project_manager import Project +from esbonio.server.features.sphinx_manager.client import ClientState from esbonio.server.features.sphinx_manager.client_subprocess import ( SubprocessSphinxClient, ) @@ -13,14 +15,11 @@ ) from esbonio.server.features.sphinx_manager.config import SphinxConfig +logger = logging.getLogger(__name__) -@pytest_lsp.fixture( - config=ClientServerConfig( - server_command=[sys.executable, "-m", "esbonio.sphinx_agent"], - client_factory=make_test_sphinx_client, - ), -) -async def client(sphinx_client: SubprocessSphinxClient, uri_for, tmp_path_factory): + +@pytest_asyncio.fixture +async def client(uri_for, tmp_path_factory): build_dir = tmp_path_factory.mktemp("build") demo_workspace = uri_for("workspaces", "demo") test_uri = demo_workspace / "index.rst" @@ -28,10 +27,11 @@ async def client(sphinx_client: SubprocessSphinxClient, uri_for, tmp_path_factor workspace = Workspace( None, workspace_folders=[ - WorkspaceFolder(uri=str(demo_workspace), name="sphinx-default"), + WorkspaceFolder(uri=str(demo_workspace), name="demo"), ], ) config = SphinxConfig( + python_command=[sys.executable], build_command=[ "sphinx-build", "-M", @@ -40,11 +40,21 @@ async def client(sphinx_client: SubprocessSphinxClient, uri_for, tmp_path_factor str(build_dir), ], ) - resolved = config.resolve(test_uri, workspace, sphinx_client.logger) + resolved = config.resolve(test_uri, workspace, logger) assert resolved is not None - info = await sphinx_client.create_application(resolved) - assert info is not None + sphinx_client = await make_test_sphinx_client(resolved) + assert sphinx_client.state == ClientState.Running await sphinx_client.build() - yield + yield sphinx_client + + await sphinx_client.stop() + + +@pytest_asyncio.fixture +async def project(client: SubprocessSphinxClient): + project = Project(client.db) + + yield project + await project.close() diff --git a/lib/esbonio/tests/sphinx-agent/handlers/test_database.py b/lib/esbonio/tests/sphinx-agent/handlers/test_database.py index cbb1ddc37..f8b55eeb7 100644 --- a/lib/esbonio/tests/sphinx-agent/handlers/test_database.py +++ b/lib/esbonio/tests/sphinx-agent/handlers/test_database.py @@ -1,5 +1,6 @@ import pytest +from esbonio.server.features.project_manager import Project from esbonio.server.features.sphinx_manager.client_subprocess import ( SubprocessSphinxClient, ) @@ -14,26 +15,35 @@ def anuri(base, *args): @pytest.mark.asyncio -async def test_files_table(client: SubprocessSphinxClient): +async def test_files_table(client: SubprocessSphinxClient, project: Project): """Ensure that we can correctly index all the files in the Sphinx project.""" src = client.src_uri - assert src is not None - assert client.db is not None - cursor = await client.db.execute("SELECT * FROM files") + db = await project.get_db() + cursor = await db.execute("SELECT * FROM files") results = await cursor.fetchall() actual = {r for r in results if "badfile" not in r[1]} expected = { (anuri(src, "index.rst"), "index", "index.html"), (anuri(src, "rst", "directives.rst"), "rst/directives", "rst/directives.html"), + ( + anuri(src, "rst", "diagnostics.rst"), + "rst/diagnostics", + "rst/diagnostics.html", + ), (anuri(src, "rst", "symbols.rst"), "rst/symbols", "rst/symbols.html"), ( anuri(src, "myst", "directives.md"), "myst/directives", "myst/directives.html", ), + ( + anuri(src, "myst", "diagnostics.md"), + "myst/diagnostics", + "myst/diagnostics.html", + ), (anuri(src, "myst", "symbols.md"), "myst/symbols", "myst/symbols.html"), (anuri(src, "demo_rst.rst"), "demo_rst", "demo_rst.html"), (anuri(src, "demo_myst.md"), "demo_myst", "demo_myst.html"), diff --git a/lib/esbonio/tests/sphinx-agent/handlers/test_diagnostics.py b/lib/esbonio/tests/sphinx-agent/handlers/test_diagnostics.py index 829f9e34e..63b823e84 100644 --- a/lib/esbonio/tests/sphinx-agent/handlers/test_diagnostics.py +++ b/lib/esbonio/tests/sphinx-agent/handlers/test_diagnostics.py @@ -7,6 +7,7 @@ from pygls.protocol import default_converter from esbonio.server import Uri +from esbonio.server.features.project_manager import Project from esbonio.server.features.sphinx_manager.client_subprocess import ( SubprocessSphinxClient, ) @@ -29,43 +30,26 @@ def check_diagnostics( @pytest.mark.asyncio -@pytest.mark.skip -async def test_diagnostics(client: SubprocessSphinxClient, uri_for): +async def test_diagnostics(client: SubprocessSphinxClient, project: Project, uri_for): """Ensure that the sphinx agent reports diagnostics collected during the build, and that they are correctly reset when fixed.""" - definitions_uri = uri_for("sphinx-default/workspace/definitions.rst") - options_uri = uri_for("sphinx-default/workspace/directive_options.rst") + rst_diagnostics_uri = uri_for("workspaces/demo/rst/diagnostics.rst") + myst_diagnostics_uri = uri_for("workspaces/demo/myst/diagnostics.md") expected = { - definitions_uri: [ + rst_diagnostics_uri: [ types.Diagnostic( - message="image file not readable: _static/bad.png", + message="image file not readable: not-an-image.png", severity=types.DiagnosticSeverity.Warning, range=types.Range( - start=types.Position(line=28, character=0), - end=types.Position(line=29, character=0), - ), - ), - types.Diagnostic( - message="unknown document: '/changelog'", - severity=types.DiagnosticSeverity.Warning, - range=types.Range( - start=types.Position(line=13, character=0), - end=types.Position(line=14, character=0), + start=types.Position(line=5, character=0), + end=types.Position(line=6, character=0), ), ), ], - options_uri: [ - types.Diagnostic( - message="image file not readable: filename.png", - severity=types.DiagnosticSeverity.Warning, - range=types.Range( - start=types.Position(line=0, character=0), - end=types.Position(line=1, character=0), - ), - ), + myst_diagnostics_uri: [ types.Diagnostic( - message="document isn't included in any toctree", + message="image file not readable: not-an-image.png", severity=types.DiagnosticSeverity.Warning, range=types.Range( start=types.Position(line=0, character=0), @@ -75,15 +59,19 @@ async def test_diagnostics(client: SubprocessSphinxClient, uri_for): ], } - actual = await client.get_diagnostics() + actual = await project.get_diagnostics() check_diagnostics(expected, actual) await client.build( - content_overrides={definitions_uri.fs_path: "My Custom Title\n==============="} + content_overrides={ + str( + rst_diagnostics_uri + ): "My Custom Title\n===============\n\nThere are no images here" + } ) - actual = await client.get_diagnostics() - check_diagnostics({options_uri: expected[options_uri]}, actual) + actual = await project.get_diagnostics() + check_diagnostics({myst_diagnostics_uri: expected[myst_diagnostics_uri]}, actual) # The original diagnostics should be reported when the issues are re-introduced. # @@ -91,8 +79,8 @@ async def test_diagnostics(client: SubprocessSphinxClient, uri_for): # trick Sphinx into re-building the file. await client.build( content_overrides={ - definitions_uri.fs_path: pathlib.Path(definitions_uri).read_text() + str(rst_diagnostics_uri): pathlib.Path(rst_diagnostics_uri).read_text() } ) - actual = await client.get_diagnostics() + actual = await project.get_diagnostics() check_diagnostics(expected, actual) diff --git a/lib/esbonio/tests/sphinx-agent/test_sa_build.py b/lib/esbonio/tests/sphinx-agent/test_sa_build.py index 35e686953..f8244d0fe 100644 --- a/lib/esbonio/tests/sphinx-agent/test_sa_build.py +++ b/lib/esbonio/tests/sphinx-agent/test_sa_build.py @@ -1,14 +1,15 @@ +import logging import pathlib import re import sys import pytest -import pytest_lsp +import pytest_asyncio from lsprotocol.types import WorkspaceFolder from pygls.exceptions import JsonRpcInternalError from pygls.workspace import Workspace -from pytest_lsp import ClientServerConfig +from esbonio.server.features.sphinx_manager.client import ClientState from esbonio.server.features.sphinx_manager.client_subprocess import ( SubprocessSphinxClient, ) @@ -17,6 +18,8 @@ ) from esbonio.server.features.sphinx_manager.config import SphinxConfig +logger = logging.getLogger(__name__) + @pytest.mark.asyncio async def test_build_includes_webview_js(client: SubprocessSphinxClient, uri_for): @@ -54,9 +57,7 @@ async def test_build_content_override(client: SubprocessSphinxClient, uri_for): assert expected in index_html.read_text() await client.build( - content_overrides={ - (src / "index.rst").fs_path: "My Custom Title\n===============" - } + content_overrides={str(src / "index.rst"): "My Custom Title\n==============="} ) # Ensure the override was applied @@ -65,16 +66,8 @@ async def test_build_content_override(client: SubprocessSphinxClient, uri_for): assert expected in index_html.read_text() -@pytest_lsp.fixture( - scope="module", - config=ClientServerConfig( - server_command=[sys.executable, "-m", "esbonio.sphinx_agent"], - client_factory=make_test_sphinx_client, - ), -) -async def client_build_error( - sphinx_client: SubprocessSphinxClient, uri_for, tmp_path_factory -): +@pytest_asyncio.fixture(scope="module") +async def client_build_error(uri_for, tmp_path_factory): """A sphinx client that will error when a build is triggered.""" build_dir = tmp_path_factory.mktemp("build") demo_workspace = uri_for("workspaces", "demo") @@ -89,6 +82,7 @@ async def client_build_error( conf_dir = uri_for("workspaces", "demo-error-build").fs_path config = SphinxConfig( + python_command=[sys.executable], build_command=[ "sphinx-build", "-b", @@ -99,13 +93,15 @@ async def client_build_error( str(build_dir), ], ) - resolved = config.resolve(test_uri, workspace, sphinx_client.logger) + resolved = config.resolve(test_uri, workspace, logger) assert resolved is not None - info = await sphinx_client.create_application(resolved) - assert info is not None + sphinx_client = await make_test_sphinx_client(resolved) + assert sphinx_client.state == ClientState.Running + + yield sphinx_client - yield + await sphinx_client.stop() @pytest.mark.asyncio(scope="module") diff --git a/lib/esbonio/tests/sphinx-agent/test_sa_create_app.py b/lib/esbonio/tests/sphinx-agent/test_sa_create_app.py index ca235bfd7..0b95e0457 100644 --- a/lib/esbonio/tests/sphinx-agent/test_sa_create_app.py +++ b/lib/esbonio/tests/sphinx-agent/test_sa_create_app.py @@ -1,13 +1,12 @@ +import logging import sys import pytest -import pytest_lsp from lsprotocol.types import WorkspaceFolder from pygls import IS_WIN -from pygls.exceptions import JsonRpcInternalError from pygls.workspace import Workspace -from pytest_lsp import ClientServerConfig +from esbonio.server.features.sphinx_manager.client import ClientState from esbonio.server.features.sphinx_manager.client_subprocess import ( SubprocessSphinxClient, ) @@ -16,19 +15,11 @@ ) from esbonio.server.features.sphinx_manager.config import SphinxConfig - -@pytest_lsp.fixture( - config=ClientServerConfig( - server_command=[sys.executable, "-m", "esbonio.sphinx_agent"], - client_factory=make_test_sphinx_client, - ) -) -async def client(sphinx_client: SubprocessSphinxClient): - yield +logger = logging.getLogger("__name__") @pytest.mark.asyncio -async def test_create_application(client: SubprocessSphinxClient, uri_for): +async def test_create_application(uri_for): """Ensure that we can create a Sphinx application instance correctly.""" demo_workspace = uri_for("workspaces", "demo") @@ -40,29 +31,35 @@ async def test_create_application(client: SubprocessSphinxClient, uri_for): WorkspaceFolder(uri=str(demo_workspace), name="demo"), ], ) - config = SphinxConfig() - resolved = config.resolve(test_uri, workspace, client.logger) + config = SphinxConfig(python_command=[sys.executable]) + resolved = config.resolve(test_uri, workspace, logger) assert resolved is not None - - info = await client.create_application(resolved) - assert info is not None - assert info.builder_name == "dirhtml" - - # Paths are case insensitive on Windows - if IS_WIN: - assert info.src_dir.lower() == demo_workspace.fs_path.lower() - assert info.conf_dir.lower() == demo_workspace.fs_path.lower() - assert "cache" in info.build_dir.lower() - else: - assert info.src_dir == demo_workspace.fs_path - assert info.conf_dir == demo_workspace.fs_path - assert "cache" in info.build_dir + client = None + + try: + client = await make_test_sphinx_client(resolved) + assert client.state == ClientState.Running + + info = client.sphinx_info + assert info is not None + assert info.builder_name == "dirhtml" + + # Paths are case insensitive on Windows + if IS_WIN: + assert info.src_dir.lower() == demo_workspace.fs_path.lower() + assert info.conf_dir.lower() == demo_workspace.fs_path.lower() + assert "cache" in info.build_dir.lower() + else: + assert info.src_dir == demo_workspace.fs_path + assert info.conf_dir == demo_workspace.fs_path + assert "cache" in info.build_dir + finally: + if client: + await client.stop() @pytest.mark.asyncio -async def test_create_application_error( - client: SubprocessSphinxClient, uri_for, tmp_path_factory -): +async def test_create_application_error(uri_for, tmp_path_factory): """Ensure that we can handle errors during application creation.""" build_dir = tmp_path_factory.mktemp("build") @@ -78,6 +75,7 @@ async def test_create_application_error( conf_dir = uri_for("workspaces", "demo-error").fs_path config = SphinxConfig( + python_command=[sys.executable], build_command=[ "sphinx-build", "-b", @@ -86,13 +84,16 @@ async def test_create_application_error( conf_dir, demo_workspace.fs_path, str(build_dir), - ] + ], ) - resolved = config.resolve(test_uri, workspace, client.logger) + resolved = config.resolve(test_uri, workspace, logger) assert resolved is not None - with pytest.raises( - JsonRpcInternalError, - match="There is a programmable error in your configuration file:", - ): - await client.create_application(resolved) + try: + client = await SubprocessSphinxClient(resolved) + assert client.state == ClientState.Errored + + message = "There is a programmable error in your configuration file:" + assert message in str(client.exception) + finally: + await client.stop() diff --git a/lib/esbonio/tests/sphinx-agent/test_sa_unit.py b/lib/esbonio/tests/sphinx-agent/test_sa_unit.py index b731c0237..f9668866f 100644 --- a/lib/esbonio/tests/sphinx-agent/test_sa_unit.py +++ b/lib/esbonio/tests/sphinx-agent/test_sa_unit.py @@ -1,24 +1,24 @@ +from __future__ import annotations + import logging import os import pathlib import sys -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple +import typing from unittest import mock import pytest -from lsprotocol.types import WorkspaceFolder -from pygls.workspace import Workspace -from esbonio.server import Uri -from esbonio.server.features.sphinx_manager.config import ( - SphinxConfig as SphinxAgentConfig, -) from esbonio.sphinx_agent.config import SphinxConfig -from esbonio.sphinx_agent.log import SphinxLogHandler +from esbonio.sphinx_agent.log import DiagnosticFilter + +if typing.TYPE_CHECKING: + from typing import Any + from typing import Dict + from typing import List + from typing import Optional + from typing import Tuple + logger = logging.getLogger(__name__) @@ -29,10 +29,8 @@ def application_args(**kwargs) -> Dict[str, Any]: "freshenv": False, "keep_going": False, "parallel": 1, - "status": None, "tags": [], "verbosity": 0, - "warning": None, "warningiserror": False, } @@ -432,84 +430,20 @@ def application_args(**kwargs) -> Dict[str, Any]: def test_cli_arg_handling(args: List[str], expected: Dict[str, Any]): """Ensure that we can convert ``sphinx-build`` to the correct Sphinx application options.""" - config = SphinxConfig.fromcli(args) assert config is not None - assert expected == config.to_application_args() + actual = config.to_application_args() -@pytest.mark.parametrize( - "config, uri, workspace, expected", - [ - # If everything is specified, resolve should be a no-op - ( - SphinxAgentConfig( - python_command=[sys.executable], - build_command=["sphinx-build", "-M", "html", "src", "dest"], - cwd="/path/to/workspace", - python_path=["/path/to/site-packages/esbonio/"], - ), - "file::///path/to/file.rst", - Workspace(None), - SphinxAgentConfig( - python_command=[sys.executable], - build_command=["sphinx-build", "-M", "html", "src", "dest"], - cwd="/path/to/workspace", - python_path=["/path/to/site-packages/esbonio/"], - ), - ), - # If no cwd given, we should try to pick one based on the given workspace - ( - SphinxAgentConfig( - python_command=[sys.executable], - build_command=["sphinx-build", "-M", "html", "src", "dest"], - python_path=["/path/to/site-packages/esbonio/"], - ), - "file::///path/to/file.rst", - Workspace("file:///path/to/workspace/root"), - SphinxAgentConfig( - python_command=[sys.executable], - build_command=["sphinx-build", "-M", "html", "src", "dest"], - cwd=os.path.join(".", "path", "to", "workspace", "root")[1:], - python_path=["/path/to/site-packages/esbonio/"], - ), - ), - ( - SphinxAgentConfig( - python_command=[sys.executable], - build_command=["sphinx-build", "-M", "html", "src", "dest"], - python_path=["/path/to/site-packages/esbonio/"], - ), - "file:///path/to/workspace-b/file.rst", - Workspace( - "file:///path/to/workspace/root", - workspace_folders=[ - WorkspaceFolder( - uri="file:///path/to/workspace-a", name="Workspace A" - ), - WorkspaceFolder( - uri="file:///path/to/workspace-b", name="Workspace B" - ), - ], - ), - SphinxAgentConfig( - python_command=[sys.executable], - build_command=["sphinx-build", "-M", "html", "src", "dest"], - cwd=os.path.join(".", "path", "to", "workspace-b")[1:], - python_path=["/path/to/site-packages/esbonio/"], - ), - ), - ], -) -def test_resolve_sphinx_config( - config: SphinxAgentConfig, - uri: str, - workspace: Workspace, - expected: SphinxAgentConfig, -): - """Ensure that we can resolve the client side config for the SphinxAgent - correctly.""" - assert expected == config.resolve(Uri.parse(uri), workspace, logger) + # pytest overrides stderr on windows, so if we were to put `sys.stderr` in the + # `expected` dict this test would fail as `sys.stderr` inside a test function has a + # different value. + # + # So, let's test for it here instead + assert actual.pop("status") == sys.stderr + assert actual.pop("warning") == sys.stderr + + assert expected == actual ROOT = pathlib.Path(__file__).parent.parent / "sphinx-extensions" / "workspace" @@ -529,7 +463,7 @@ def test_resolve_sphinx_config( (f"{RST_PATH}:3", (str(RST_PATH), 3)), (f"{REL_INC_PATH}:12", (str(INC_PATH), 12)), ( - f"{PY_PATH}:docstring of esbonio.sphinx_agent.log.SphinxLogHandler:3", + f"{PY_PATH}:docstring of esbonio.sphinx_agent.log.DiagnosticFilter:3", (str(PY_PATH), 22), ), (f"internal padding after {RST_PATH}:34", (str(RST_PATH), 34)), @@ -543,9 +477,9 @@ def test_get_diagnostic_location(location: str, expected: Tuple[str, Optional[in app = mock.Mock() app.confdir = str(ROOT / "sphinx-extensions") - handler = SphinxLogHandler(app) + handler = DiagnosticFilter(app) - mockpath = f"{SphinxLogHandler.__module__}.inspect.getsourcelines" + mockpath = f"{DiagnosticFilter.__module__}.inspect.getsourcelines" with mock.patch(mockpath, return_value=([""], 20)): actual = handler.get_location(location) diff --git a/lib/esbonio/tests/sphinx-agent/test_sa_uri_class.py b/lib/esbonio/tests/sphinx-agent/test_sa_uri_class.py index d81024c4a..ba21a94ab 100644 --- a/lib/esbonio/tests/sphinx-agent/test_sa_uri_class.py +++ b/lib/esbonio/tests/sphinx-agent/test_sa_uri_class.py @@ -2,6 +2,7 @@ # https://github.com/microsoft/vscode/blob/5653420433692dc4269ad39adbc143e3438af179/src/vs/base/test/common/uri.test.ts import os.path import pathlib +from typing import Any from typing import Dict import pytest @@ -30,6 +31,72 @@ def test_uri_hashable(): assert hash(uri) != 0 +@pytest.mark.parametrize( + "a,b,expected", + [ + (Uri.parse("file:///a.txt"), 1, False), + (Uri.create(scheme="file"), Uri.create(scheme="file"), True), + (Uri.create(scheme="file"), Uri.create(scheme="http"), False), + ( + Uri.create(scheme="file", query="a"), + Uri.create(scheme="file", query="a"), + True, + ), + ( + Uri.create(scheme="file", query="a"), + Uri.create(scheme="file", query="b"), + False, + ), + ( + Uri.create(scheme="file", authority="a"), + Uri.create(scheme="file", authority="a"), + True, + ), + ( + Uri.create(scheme="file", authority="a"), + Uri.create(scheme="file", authority="b"), + False, + ), + ( + Uri.create(scheme="file", fragment="a"), + Uri.create(scheme="file", fragment="a"), + True, + ), + ( + Uri.create(scheme="file", fragment="a"), + Uri.create(scheme="file", fragment="b"), + False, + ), + ( + Uri.create(scheme="http", path="a/b"), + Uri.create(scheme="http", path="a/b"), + True, + ), + ( + Uri.create(scheme="http", path="a/b"), + Uri.create(scheme="http", path="a/B"), + False, + ), + pytest.param( + Uri.create(scheme="file", path="a/b"), + Uri.create(scheme="file", path="a/B"), + False, + marks=pytest.mark.skipif(IS_WIN, reason="N/A for Windows"), + ), + pytest.param( + # Filepaths on Windows are case insensitive + Uri.create(scheme="file", path="a/b"), + Uri.create(scheme="file", path="a/B"), + True, + marks=pytest.mark.skipif(not IS_WIN, reason="Windows only"), + ), + ], +) +def test_uri_eq(a: Uri, b: Any, expected: bool): + """Ensure that we have implemented ``__eq__`` for Uris correctly.""" + assert (a == b) is expected + + @pytest.mark.parametrize( "path, expected", [ diff --git a/lib/esbonio/tests/unit_tests/test_directive_completions.py b/lib/esbonio/tests/unit_tests/test_directive_completions.py deleted file mode 100644 index 31e7d6a75..000000000 --- a/lib/esbonio/tests/unit_tests/test_directive_completions.py +++ /dev/null @@ -1,109 +0,0 @@ -from functools import partial -from typing import Optional -from typing import Type - -import pytest -from docutils.parsers.rst import Directive -from docutils.parsers.rst.directives.images import Image -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionItemKind -from lsprotocol.types import TextEdit - -from esbonio.lsp import CompletionContext -from esbonio.lsp.directives.completions import render_directive_option_completion -from esbonio.lsp.testing import make_completion_context -from esbonio.lsp.testing import range_from_str -from esbonio.lsp.util.patterns import DIRECTIVE_OPTION - -make_directive_option_completion_context = partial( - make_completion_context, DIRECTIVE_OPTION -) - - -@pytest.mark.parametrize( - "context, option, name, directive, expected", - [ - ( - make_directive_option_completion_context(" :"), - "align", - "image", - Image, - CompletionItem( - label="align", - filter_text=":align:", - text_edit=TextEdit(range=range_from_str("0:3-0:4"), new_text=":align:"), - ), - ), - ( - make_directive_option_completion_context(" :width"), - "align", - "image", - Image, - CompletionItem( - label="align", - filter_text=":align:", - text_edit=TextEdit(range=range_from_str("0:3-0:9"), new_text=":align:"), - ), - ), - ( - make_directive_option_completion_context(" :width", prefer_insert=True), - "align", - "image", - Image, - None, - ), - ( - make_directive_option_completion_context(" :fi", prefer_insert=True), - "figwidth", - "image", - Image, - CompletionItem(label="figwidth", insert_text="figwidth:"), - ), - ( - make_directive_option_completion_context(" :sh", prefer_insert=True), - "show-caption", - "image", - Image, - CompletionItem(label="show-caption", insert_text="show-caption:"), - ), - ( - make_directive_option_completion_context(" :show-", prefer_insert=True), - "show-caption", - "image", - Image, - CompletionItem(label="show-caption", insert_text="caption:"), - ), - ( - make_directive_option_completion_context(" :show-c", prefer_insert=True), - "show-caption", - "image", - Image, - CompletionItem(label="show-caption", insert_text="caption:"), - ), - ], -) -def test_render_directive_option_completion( - context: CompletionContext, - option: str, - name: str, - directive: Type[Directive], - expected: Optional[CompletionItem], -): - """Ensure that we can render directive options completions correctly, according to - the current context.""" - - # These fields are always present, so let's not force the test author to add them - # in :) - if expected is not None: - expected.kind = CompletionItemKind.Field - expected.data = {"completion_type": "directive_option", "for_directive": name} - - try: - expected.detail = f"{directive.__module__}.{directive.__name__}:{option}" - except AttributeError: - expected.detail = ( - f"{directive.__module__}.{directive.__class__.__name__}:{option}" - ) - - actual = render_directive_option_completion(context, option, name, directive) - assert actual == expected diff --git a/lib/esbonio/tests/unit_tests/test_directives.py b/lib/esbonio/tests/unit_tests/test_directives.py deleted file mode 100644 index fb37833c3..000000000 --- a/lib/esbonio/tests/unit_tests/test_directives.py +++ /dev/null @@ -1,331 +0,0 @@ -import sys -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional -from typing import Tuple - -import pytest -from docutils.parsers.rst import Directive -from lsprotocol.types import Location -from lsprotocol.types import Position -from lsprotocol.types import Range - -from esbonio.lsp import CompletionContext -from esbonio.lsp import DefinitionContext -from esbonio.lsp.directives import DirectiveLanguageFeature -from esbonio.lsp.directives import Directives -from esbonio.lsp.rst import DocumentLinkContext -from esbonio.lsp.rst.config import ServerCompletionConfig - -if sys.version_info.minor < 8: - from mock import Mock -else: - from unittest.mock import Mock - - -class Simple(DirectiveLanguageFeature): - """A simple directive language feature for use in tests.""" - - def __init__(self, names: List[str]): - self.directives = {name: Directive for name in names} - - def index_directives(self) -> Dict[str, Directive]: - return self.directives - - def suggest_options( - self, context: CompletionContext, directive: str, domain: Optional[str] - ) -> Iterable[str]: - return iter(directive) if directive in self.directives else [] - - # The default `suggest_directives` implementation should be sufficient. - # The default `get_implementation` implementation should be sufficient. - - def find_argument_definitions( - self, - context: DefinitionContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> List[Location]: - if directive not in self.directives: - return [] - - return [ - Location( - uri=f"file:///{directive}.rst", - range=Range( - start=Position(line=1, character=0), - end=Position(line=2, character=0), - ), - ) - ] - - def resolve_argument_link( - self, - context: DocumentLinkContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> Tuple[Optional[str], Optional[str]]: - if directive not in self.directives: - return None, None - - return f"file:///{directive}.rst", None - - -class Broken(DirectiveLanguageFeature): - """A directive language feature that only throws exceptions.""" - - def index_directives(self) -> Dict[str, Directive]: - raise NotImplementedError() - - def suggest_options( - self, context: CompletionContext, directive: str, domain: Optional[str] - ) -> Iterable[str]: - raise NotImplementedError() - - # The default `suggest_directives` implementation should be sufficient. - # The default `get_implementation` implementation should be sufficient. - - def find_argument_definitions( - self, - context: DefinitionContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> List[Location]: - raise NotImplementedError() - - def resolve_argument_link( - self, - context: DocumentLinkContext, - directive: str, - domain: Optional[str], - argument: str, - ) -> Tuple[Optional[str], Optional[str]]: - raise NotImplementedError() - - -@pytest.fixture() -def simple(): - """A simple functional instance of the directive language feature""" - - f1 = Simple(["one", "two"]) - f2 = Simple(["three", "four"]) - - directives = Directives(Mock()) - directives.add_feature(f1) - directives.add_feature(f2) - - return directives - - -@pytest.fixture() -def broken(): - """A an instance of the directive language feature with sub features that will - throw errors.""" - - f1 = Simple(["one", "two"]) - f2 = Broken() - f3 = Simple(["three", "four"]) - - directives = Directives(Mock()) - directives.add_feature(f1) - directives.add_feature(f2) - directives.add_feature(f3) - - return directives - - -def test_get_directives(simple: Directives): - """Ensure that we can correctly combine directives from multiple sources.""" - - items = simple.get_directives() - assert list(items.keys()) == ["one", "two", "three", "four"] - assert all([cls == Directive for cls in items.values()]) - - # All should be well - simple.logger.error.assert_not_called() - - -def test_get_directives_error(broken: Directives): - """Ensure we can gracefully handle errors in directive language features.""" - - items = broken.get_directives() - assert list(items.keys()) == ["one", "two", "three", "four"] - assert all([cls == Directive for cls in items.values()]) - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to index directives") - - -def test_get_implementation(simple: Directives): - """Ensure that we can correctly look up directives from multiple sources.""" - - impl = simple.get_implementation("one", None) - assert impl == Directive - - impl = simple.get_implementation("four", None) - assert impl == Directive - - # All should be well - simple.logger.error.assert_not_called() - - -def test_get_implementation_error(broken: Directives): - """Ensure we can gracefully handle errors in directive language features.""" - - impl = broken.get_implementation("four", None) - assert impl == Directive - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to get implementation for") - - -def test_suggest_directives(simple: Directives): - """Ensure that we can correctly combine directives from multiple sources.""" - - context = CompletionContext( - doc=Mock(), - location="rst", - match=Mock(), - position=Mock(), - config=ServerCompletionConfig(), - capabilities=Mock(), - ) - items = simple.suggest_directives(context) - - assert [i[0] for i in items] == ["one", "two", "three", "four"] - assert all([i[1] == Directive for i in items]) - - # All should be well - simple.logger.error.assert_not_called() - - -def test_suggest_directives_error(broken: Directives): - """Ensure that we can gracefully handle errors in directive language features.""" - - context = CompletionContext( - doc=Mock(), - location="rst", - match=Mock(), - position=Mock(), - config=ServerCompletionConfig(), - capabilities=Mock(), - ) - items = broken.suggest_directives(context) - - assert [i[0] for i in items] == ["one", "two", "three", "four"] - assert all([i[1] == Directive for i in items]) - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to suggest directives") - - -def test_suggest_options(simple: Directives): - """Ensure that we can correctly combine directives from multiple sources.""" - - context = CompletionContext( - doc=Mock(), - location="rst", - match=Mock(), - position=Mock(), - config=ServerCompletionConfig(), - capabilities=Mock(), - ) - - items = simple.suggest_options(context, "four", None) - assert list(items) == ["f", "o", "u", "r"] - - # All should be well - simple.logger.error.assert_not_called() - - -def test_suggest_options_error(broken: Directives): - """Ensure that we can gracefully handle errors in directive language features.""" - - context = CompletionContext( - doc=Mock(), - location="rst", - match=Mock(), - position=Mock(), - config=ServerCompletionConfig(), - capabilities=Mock(), - ) - - items = broken.suggest_options(context, "four", None) - assert list(items) == ["f", "o", "u", "r"] - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to suggest options for ") - - -def test_find_argument_definitions(simple: Directives): - """Ensure that we can correctly combine definitions from multiple sources.""" - - context = DefinitionContext( - doc=Mock(), location="rst", match=Mock(), position=Mock() - ) - - locations = simple.find_argument_definition(context, "one", "", "example") - assert locations[0].uri == "file:///one.rst" - - locations = simple.find_argument_definition(context, "four", "", "example") - assert locations[0].uri == "file:///four.rst" - - # All should be well - simple.logger.error.assert_not_called() - - -def test_find_argument_definitions_error(broken: Directives): - """Ensure that we can gracefully handle errors in directive language features.""" - - context = DefinitionContext( - doc=Mock(), location="rst", match=Mock(), position=Mock() - ) - - locations = broken.find_argument_definition(context, "four", "", "example") - assert locations[0].uri == "file:///four.rst" - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to find definitions") - - -def test_resolve_argument_link(simple: Directives): - """Ensure that we can use multiple sources to resolve a document link.""" - - context = DocumentLinkContext(doc=Mock(), capabilities=Mock()) - - target, _ = simple.resolve_argument_link(context, "one", "", "example") - assert target == "file:///one.rst" - - target, _ = simple.resolve_argument_link(context, "four", "", "example") - assert target == "file:///four.rst" - - # All should be well - simple.logger.error.assert_not_called() - - -def test_resolve_argument_link_error(broken: Directives): - """Ensure that we gracefully handle errors when resolving argument links.""" - - context = DocumentLinkContext(doc=Mock(), capabilities=Mock()) - - target, _ = broken.resolve_argument_link(context, "four", "", "example") - assert target == "file:///four.rst" - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to resolve argument link") diff --git a/lib/esbonio/tests/unit_tests/test_log.py b/lib/esbonio/tests/unit_tests/test_log.py deleted file mode 100644 index 6b2a68cd1..000000000 --- a/lib/esbonio/tests/unit_tests/test_log.py +++ /dev/null @@ -1,87 +0,0 @@ -import logging -import sys - -import pytest -from lsprotocol.types import DiagnosticSeverity -from lsprotocol.types import DiagnosticTag -from pygls import IS_WIN - -from esbonio.lsp.log import LspHandler - -if sys.version_info.minor < 8: - from mock import Mock -else: - from unittest.mock import Mock - - -@pytest.mark.parametrize( - "message,expected", - [ - ( - "/path/to/file.py:108: Warning: Here is a warning message", - { - "uri": "file:///path/to/file.py", - "line": 108, - "message": "Here is a warning message", - "deprecated": False, - }, - ), - ( - "/path/to/file.py:18: DeprecationWarning: Here is a warning message", - { - "uri": "file:///path/to/file.py", - "line": 18, - "message": "Here is a warning message", - "deprecated": True, - }, - ), - ( - "/path/to/file.py:xx: DeprecationWarning: Here is a warning message", - { - "uri": "file:///path/to/file.py", - "line": 1, - "message": "Here is a warning message", - "deprecated": True, - }, - ), - pytest.param( - "c:\\path\\to\\file.py:18: DeprecationWarning: Here is a warning message", - { - "uri": "file:///c:/path/to/file.py", - "line": 18, - "message": "Here is a warning message", - "deprecated": True, - }, - marks=pytest.mark.skipif(not IS_WIN, reason="test only valid on Windows"), - ), - ], -) -def test_handle_warning(message: str, expected: dict): - """Ensure that we can parse warning messages correctly.""" - - server = Mock() - handler = LspHandler(server, True) - - record = logging.LogRecord( - "testname", - logging.WARNING, - "/path/to/file.py", - 12, - "%s", - (message,), - exc_info=None, - ) - handler.handle_warning(record) - - namespace, uri, diagnostic = server.add_diagnostics.call_args.args - - assert namespace == "esbonio" - assert uri == expected["uri"] - - assert diagnostic.message == expected["message"] - assert diagnostic.severity == DiagnosticSeverity.Warning - assert diagnostic.range.start.line == expected["line"] - 1 - assert diagnostic.range.end.line == expected["line"] - - if expected["deprecated"]: - assert diagnostic.tags == [DiagnosticTag.Deprecated] diff --git a/lib/esbonio/tests/unit_tests/test_role_completions.py b/lib/esbonio/tests/unit_tests/test_role_completions.py deleted file mode 100644 index d9ad9e3a7..000000000 --- a/lib/esbonio/tests/unit_tests/test_role_completions.py +++ /dev/null @@ -1,135 +0,0 @@ -from functools import partial -from typing import Any -from typing import Optional - -import pytest -from lsprotocol.types import CompletionItem -from lsprotocol.types import CompletionItemKind -from lsprotocol.types import TextEdit -from sphinx.domains.cpp import CPPXRefRole -from sphinx.roles import XRefRole - -from esbonio.lsp import CompletionContext -from esbonio.lsp.roles.completions import render_role_completion -from esbonio.lsp.testing import make_completion_context -from esbonio.lsp.testing import range_from_str -from esbonio.lsp.util.patterns import ROLE - -make_role_completion_context = partial(make_completion_context, ROLE) - - -@pytest.mark.parametrize( - "context, name, role, expected", - [ - ( - make_role_completion_context(":"), - "ref", - XRefRole, - CompletionItem( - label="ref", - filter_text=":ref:", - text_edit=TextEdit(range=range_from_str("0:0-0:1"), new_text=":ref:"), - ), - ), - ( - make_role_completion_context(":r"), - "ref", - XRefRole, - CompletionItem( - label="ref", - filter_text=":ref:", - text_edit=TextEdit(range=range_from_str("0:0-0:2"), new_text=":ref:"), - ), - ), - ( - make_role_completion_context(":doc"), - "ref", - XRefRole, - CompletionItem( - label="ref", - filter_text=":ref:", - text_edit=TextEdit(range=range_from_str("0:0-0:4"), new_text=":ref:"), - ), - ), - ( - make_role_completion_context(":c"), - "cpp:func", - CPPXRefRole, - CompletionItem( - label="cpp:func", - filter_text=":cpp:func:", - text_edit=TextEdit( - range=range_from_str("0:0-0:2"), new_text=":cpp:func:" - ), - ), - ), - ( - make_role_completion_context(":cpp:f"), - "cpp:func", - CPPXRefRole, - CompletionItem( - label="cpp:func", - filter_text=":cpp:func:", - text_edit=TextEdit( - range=range_from_str("0:0-0:6"), new_text=":cpp:func:" - ), - ), - ), - ( - make_role_completion_context(":", prefer_insert=True), - "ref", - XRefRole, - CompletionItem(label="ref", insert_text="ref:"), - ), - ( - make_role_completion_context(":r", prefer_insert=True), - "ref", - XRefRole, - CompletionItem(label="ref", insert_text="ref:"), - ), - ( - make_role_completion_context(":doc", prefer_insert=True), - "ref", - XRefRole, - None, - ), - ( - make_role_completion_context(":c", prefer_insert=True), - "cpp:func", - CPPXRefRole, - CompletionItem(label="cpp:func", insert_text="cpp:func:"), - ), - ( - make_role_completion_context(":cpp:", prefer_insert=True), - "cpp:func", - CPPXRefRole, - CompletionItem(label="cpp:func", insert_text="func:"), - ), - ( - make_role_completion_context(":cpp:f", prefer_insert=True), - "cpp:func", - CPPXRefRole, - CompletionItem(label="cpp:func", insert_text="func:"), - ), - ], -) -def test_render_role_completion( - context: CompletionContext, name: str, role: Any, expected: Optional[CompletionItem] -): - """Ensure that we can render role completions correctly, according to the current - context.""" - - # These fields are always present, so let's not force the test author to add them - # each time :) - - if expected is not None: - expected.kind = CompletionItemKind.Function - expected.data = {"completion_type": "role"} - - try: - expected.detail = f"{role.__module__}.{role.__name__}" - except AttributeError: - expected.detail = f"{role.__module__}.{role.__class__.__name__}" - - actual = render_role_completion(context, name, role) - assert actual == expected diff --git a/lib/esbonio/tests/unit_tests/test_roles.py b/lib/esbonio/tests/unit_tests/test_roles.py deleted file mode 100644 index b65b258bc..000000000 --- a/lib/esbonio/tests/unit_tests/test_roles.py +++ /dev/null @@ -1,325 +0,0 @@ -import logging -import sys -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple - -import pytest -from lsprotocol.types import CompletionItem -from lsprotocol.types import Location -from lsprotocol.types import Position -from lsprotocol.types import Range - -from esbonio.lsp import CompletionContext -from esbonio.lsp import DefinitionContext -from esbonio.lsp import DocumentLinkContext -from esbonio.lsp.roles import RoleLanguageFeature -from esbonio.lsp.roles import Roles -from esbonio.lsp.rst.config import ServerCompletionConfig - -if sys.version_info.minor < 8: - from mock import Mock -else: - from unittest.mock import Mock - - -logger = logging.getLogger(__name__) - - -class Simple(RoleLanguageFeature): - """A simple role language feature for use in tests.""" - - def __init__(self, names: List[str]): - self.roles = {name: lambda x: x for name in names} - - def index_roles(self) -> Dict[str, Any]: - """Return all known roles.""" - return self.roles - - def complete_targets( - self, context: CompletionContext, name: str, domain: Optional[str] - ) -> List[CompletionItem]: - if name not in self.roles: - return [] - - return [CompletionItem(label=f"{r}-{name}") for r in self.roles] - - def find_target_definitions( - self, context: DefinitionContext, name: str, domain: str, label: str - ) -> List[Location]: - if name not in self.roles: - return [] - - return [ - Location( - uri=f"file:///{name}.rst", - range=Range( - start=Position(line=1, character=0), - end=Position(line=2, character=0), - ), - ) - ] - - def resolve_target_link( - self, context: DocumentLinkContext, name: str, domain: Optional[str], label: str - ) -> Tuple[Optional[str], Optional[str]]: - if name not in self.roles: - return None, None - - return f"file:///{name}.rst", None - - # The default `suggest_roles` implementation should be sufficient. - # The default `get_implementation` implementation should be sufficient. - - -class Broken(RoleLanguageFeature): - """A role language feature that only throws exceptions.""" - - def index_roles(self) -> Dict[str, Any]: - """Return all known roles.""" - raise NotImplementedError() - - def complete_targets( - self, context: CompletionContext, name: str, domain: Optional[str] - ) -> List[CompletionItem]: - raise NotImplementedError() - - def find_target_definitions( - self, context: DefinitionContext, name: str, domain: str, label: str - ) -> List[Location]: - raise NotImplementedError() - - def resolve_target_link( - self, context: DocumentLinkContext, name: str, domain: Optional[str], label: str - ) -> Tuple[Optional[str], Optional[str]]: - raise NotImplementedError() - - # The default `suggest_roles` implementation should be sufficient. - # The default `get_implementation` implementation should be sufficient. - - -@pytest.fixture() -def simple(): - """A simple functional instance of the roles language feature""" - - f1 = Simple(["one", "two"]) - f2 = Simple(["three", "four"]) - - roles = Roles(Mock()) - roles.add_feature(f1) - roles.add_feature(f2) - - return roles - - -@pytest.fixture() -def broken(): - """An instance of the roles language feature with sub features that will throw - errors.""" - - f1 = Simple(["one", "two"]) - f2 = Broken() - f3 = Simple(["three", "four"]) - - roles = Roles(Mock()) - roles.add_feature(f1) - roles.add_feature(f2) - roles.add_feature(f3) - - return roles - - -def test_get_roles(simple: Roles): - """Ensure that we can correctly combine roles from multiple sources.""" - - items = simple.get_roles() - assert list(items.keys()) == ["one", "two", "three", "four"] - - # All should be well - simple.logger.error.assert_not_called() - - -def test_get_roles_error(broken: Roles): - """Ensure that we can gracefully handle errors in role langauge features.""" - - items = broken.get_roles() - assert list(items.keys()) == ["one", "two", "three", "four"] - - # The error should have been logged. - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to index roles") - - -def test_get_implementation(simple: Roles): - """Ensure that we can correctly look up roles from multiple sources.""" - - impl = simple.get_implementation("one", None) - assert callable(impl) - - impl = simple.get_implementation("four", None) - assert callable(impl) - - # All should be well - simple.logger.error.assert_not_called() - - -def test_get_implementation_error(broken: Roles): - """Ensure that we can gracefully handle errors in role language features.""" - - impl = broken.get_implementation("four", None) - assert callable(impl) - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to get implementation for") - - -def test_suggest_roles(simple: Roles): - """Ensure that we can correctly combine roles from multiple sources.""" - - context = CompletionContext( - doc=Mock(), - location="rst", - match=Mock(), - position=Mock(), - config=ServerCompletionConfig(), - capabilities=Mock(), - ) - - items = simple.suggest_roles(context) - assert [i[0] for i in items] == ["one", "two", "three", "four"] - assert all([callable(i[1]) for i in items]) - - # All should be well - simple.logger.error.assert_not_called() - - -def test_suggest_roles_error(broken: Roles): - """Ensure that we can gracefully handle errors in role language features.""" - - context = CompletionContext( - doc=Mock(), - location="rst", - match=Mock(), - position=Mock(), - config=ServerCompletionConfig(), - capabilities=Mock(), - ) - - items = broken.suggest_roles(context) - assert [i[0] for i in items] == ["one", "two", "three", "four"] - assert all([callable(i[1]) for i in items]) - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to suggest roles") - - -def test_find_target_definitions(simple: Roles): - """Ensure that we can find target definitions using multiple sources.""" - - context = DefinitionContext( - doc=Mock(), location="rst", match=Mock(), position=Mock() - ) - - locations = simple.find_target_definitions(context, "one", "", "example") - assert locations[0].uri == "file:///one.rst" - - locations = simple.find_target_definitions(context, "four", "", "example") - assert locations[0].uri == "file:///four.rst" - - # All should be well - simple.logger.error.assert_not_called() - - -def test_find_target_definitions_error(broken: Roles): - """Ensure that we can gracefully handle errors in role language features.""" - - context = DefinitionContext( - doc=Mock(), location="rst", match=Mock(), position=Mock() - ) - - locations = broken.find_target_definitions(context, "four", "", "example") - assert locations[0].uri == "file:///four.rst" - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to find definitions") - - -def test_resolve_target_link(simple: Roles): - """Ensure that we can resolve links using multiple sources.""" - - context = DocumentLinkContext(doc=Mock(), capabilities=Mock()) - - target, _ = simple.resolve_target_link(context, "one", "", "example") - assert target == "file:///one.rst" - - target, _ = simple.resolve_target_link(context, "four", "", "example") - assert target == "file:///four.rst" - - # All should be well - simple.logger.error.assert_not_called() - - -def test_resolve_target_link_error(broken: Roles): - """Ensure that we can gracefully handle errors in role language features.""" - - context = DocumentLinkContext(doc=Mock(), capabilities=Mock()) - - target, _ = broken.resolve_target_link(context, "four", "", "example") - assert target == "file:///four.rst" - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to resolve target link") - - -def test_suggest_targets(simple: Roles): - """Ensure that we can collect target completions from multiple sources.""" - - context = CompletionContext( - doc=Mock(), - location="rst", - match=Mock(), - position=Mock(), - config=ServerCompletionConfig(), - capabilities=Mock(), - ) - - items = simple.suggest_targets(context, "one", "") - assert [i.label for i in items] == ["one-one", "two-one"] - - items = simple.suggest_targets(context, "four", "") - assert [i.label for i in items] == ["three-four", "four-four"] - - # All should be well - simple.logger.error.assert_not_called() - - -def test_suggest_targets_error(broken: Roles): - """Ensure that we can gracefully handle errors in role language features.""" - - context = CompletionContext( - doc=Mock(), - location="rst", - match=Mock(), - position=Mock(), - config=ServerCompletionConfig(), - capabilities=Mock(), - ) - - items = broken.suggest_targets(context, "four", "") - assert [i.label for i in items] == ["three-four", "four-four"] - - # The error should've been logged - broken.logger.error.assert_called_once() - args = broken.logger.error.call_args.args - assert args[0].startswith("Unable to suggest targets for") diff --git a/lib/esbonio/tests/unit_tests/test_rst.py b/lib/esbonio/tests/unit_tests/test_rst.py deleted file mode 100644 index 009ec3f6c..000000000 --- a/lib/esbonio/tests/unit_tests/test_rst.py +++ /dev/null @@ -1,161 +0,0 @@ -import pytest - -from esbonio.lsp.roles import Roles -from esbonio.lsp.rst import RstLanguageServer -from esbonio.lsp.rst import _get_setup_arguments -from esbonio.lsp.sphinx import SphinxLanguageServer - - -def test_get_feature_by_string(event_loop): - """Ensure that a language feature can be retrieved by a string, but raises a - deprecation warning.""" - - rst = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - expected = Roles(rst) - - rst.add_feature(expected) - key = f"{expected.__module__}.{expected.__class__.__name__}" - - with pytest.deprecated_call(): - actual = rst.get_feature(key) - - assert actual is expected - - -def test_get_feature_by_cls(event_loop): - """Ensure that a language feature can be retrieved via its class definition.""" - - rst = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - expected = Roles(rst) - - rst.add_feature(expected) - actual = rst.get_feature(Roles) - - assert actual is expected - - -def test_get_missing_feature_by_string(event_loop): - """Ensure that if a language feature is missing ``None`` is returned, but a - deprecation warning is raised.""" - - rst = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - - with pytest.deprecated_call(): - actual = rst.get_feature("xxx") - - assert actual is None - - -def test_get_missing_feature_by_cls(event_loop): - """Ensure that if a language feature is missing ``None`` is returned.""" - - rst = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - assert rst.get_feature(Roles) is None - - -def test_get_setup_arguments_rst_server(event_loop): - """Ensure that we can correctly construct the set of arguments to pass to a module's - setup function.""" - - def setup(rst: RstLanguageServer): - ... - - server = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - args = _get_setup_arguments(server, setup, "modname") - - assert args == {"rst": server} - - -def test_get_setup_arguments_server_superclass(event_loop): - """If the setup function is not compatible with the given server it should be - skipped.""" - - def setup(rst: SphinxLanguageServer): - ... - - server = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - args = _get_setup_arguments(server, setup, "modname") - - assert args is None - - -def test_get_setup_arguments_sphinx_server(event_loop): - """Ensure that we can correctly construct the set of arguments to pass to a module's - setup function.""" - - def setup(ls: SphinxLanguageServer): - ... - - server = SphinxLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - args = _get_setup_arguments(server, setup, "modname") - - assert args == {"ls": server} - - -def test_get_setup_arguments_server_subclass(event_loop): - """Ensure that we can correctly construct the set of arguments to pass to a module's - setup function.""" - - def setup(ls: RstLanguageServer): - ... - - server = SphinxLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - args = _get_setup_arguments(server, setup, "modname") - - assert args == {"ls": server} - - -def test_get_setup_arguments_server_and_feature(event_loop): - """We should also be able to automatically pass the correct language features""" - - def setup(rst: RstLanguageServer, rs: Roles): - ... - - server = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - - roles = Roles(server) - server.add_feature(roles) - - args = _get_setup_arguments(server, setup, "modname") - - assert args == {"rst": server, "rs": roles} - - -def test_get_setup_arguments_feature_only(event_loop): - """It should be possible to request just language features.""" - - def setup(roles: Roles): - ... - - server = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - - roles = Roles(server) - server.add_feature(roles) - - args = _get_setup_arguments(server, setup, "modname") - - assert args == {"roles": roles} - - -def test_get_setup_arguments_missing_feature(event_loop): - """If a requested feature is not available the function should be skipped.""" - - def setup(rst: RstLanguageServer, rs: Roles): - ... - - server = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - args = _get_setup_arguments(server, setup, "modname") - - assert args is None - - -def test_get_setup_arguments_wrong_type(event_loop): - """If an unsupported type is requested it should be skipped.""" - - def setup(rst: RstLanguageServer, rs: int): - ... - - server = RstLanguageServer(name="esbonio-test", version="v0.1", loop=event_loop) - args = _get_setup_arguments(server, setup, "modname") - - assert args is None diff --git a/lib/esbonio/tests/unit_tests/test_sphinx.py b/lib/esbonio/tests/unit_tests/test_sphinx.py deleted file mode 100644 index 798ea6d80..000000000 --- a/lib/esbonio/tests/unit_tests/test_sphinx.py +++ /dev/null @@ -1,446 +0,0 @@ -import itertools -import os -import pathlib -from typing import List -from typing import Optional -from typing import Tuple -from unittest import mock - -import pygls.uris as uri -import pytest -from pygls import IS_WIN - -from esbonio.lsp.sphinx import SphinxConfig -from esbonio.lsp.sphinx import SphinxLogHandler - - -def config_with(**kwargs) -> SphinxConfig: - """Return a SphinxConfig object with the given config dir.""" - - args = {k: str(v) if isinstance(v, pathlib.Path) else v for k, v in kwargs.items()} - for dir_ in ["build_dir", "conf_dir", "doctree_dir", "src_dir"]: - if dir_ not in args: - args[dir_] = str( - pathlib.Path(f"/path/to/{dir_.replace('_dir', '')}").resolve() - ) - - return SphinxConfig(**args) - - -@pytest.mark.parametrize( - "root_uri, setup", - [ - *itertools.product( - ["file:///path/to/root"], - [ - # build_dir handling - ( - config_with(build_dir="/path/to/build"), - config_with(build_dir=pathlib.Path("/path/to/build/html")), - ), - ( - config_with(build_dir="~/path/to/build"), - config_with( - build_dir=pathlib.Path("~/path/to/build/html").expanduser() - ), - ), - ( - config_with(build_dir="${workspaceRoot}/build"), - config_with( - build_dir=pathlib.Path("/path/to/root/build/html").resolve() - ), - ), - ( - config_with(build_dir="${workspaceRoot}/../build"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve() - ), - ), - ( - config_with(build_dir="${workspaceFolder}/build"), - config_with( - build_dir=pathlib.Path("/path/to/root/build/html").resolve() - ), - ), - ( - config_with(build_dir="${workspaceFolder}/../build"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve() - ), - ), - ( - config_with(build_dir="${confDir}/build"), - config_with( - build_dir=pathlib.Path("/path/to/conf/build/html").resolve() - ), - ), - ( - config_with(build_dir="${confDir}/../build"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve() - ), - ), - ( - config_with(build_dir="/path/to/build", make_mode=False), - config_with( - build_dir=pathlib.Path("/path/to/build"), make_mode=False - ), - ), - # confDir handling - ( - config_with(conf_dir="/path/to/config"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - conf_dir=pathlib.Path("/path/to/config"), - ), - ), - ( - config_with(conf_dir="~/path/to/config"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - conf_dir=pathlib.Path("~/path/to/config").expanduser(), - ), - ), - ( - config_with(conf_dir="${workspaceRoot}/config"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - conf_dir=pathlib.Path("/path/to/root/config").resolve(), - ), - ), - ( - config_with(conf_dir="${workspaceRoot}/../config"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - conf_dir=pathlib.Path("/path/to/config").resolve(), - ), - ), - ( - config_with(conf_dir="${workspaceFolder}/config"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - conf_dir=pathlib.Path("/path/to/root/config").resolve(), - ), - ), - ( - config_with(conf_dir="${workspaceFolder}/../config"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - conf_dir=pathlib.Path("/path/to/config").resolve(), - ), - ), - # doctreeDir handling (make mode) - ( - config_with(build_dir="/path/to/build", doctree_dir=None), - config_with( - build_dir=pathlib.Path("/path/to/build/html"), - doctree_dir=pathlib.Path("/path/to/build/doctrees"), - ), - ), - ( - config_with( - build_dir="/path/to/build", doctree_dir="/path/to/doctrees" - ), - config_with( - build_dir=pathlib.Path("/path/to/build/html"), - doctree_dir=pathlib.Path("/path/to/doctrees"), - ), - ), - ( - config_with( - build_dir="/path/to/build", - doctree_dir="${workspaceRoot}/doctrees", - ), - config_with( - build_dir=pathlib.Path("/path/to/build/html"), - doctree_dir=pathlib.Path("/path/to/root/doctrees").resolve(), - ), - ), - ( - config_with( - build_dir="/path/to/build", - doctree_dir="${workspaceFolder}/../dts", - ), - config_with( - build_dir=pathlib.Path("/path/to/build/html"), - doctree_dir=pathlib.Path("/path/to/dts").resolve(), - ), - ), - ( - config_with( - build_dir="/path/to/build", - doctree_dir="${confDir}/dts", - ), - config_with( - build_dir=pathlib.Path("/path/to/build/html"), - doctree_dir=pathlib.Path("/path/to/conf/dts").resolve(), - ), - ), - ( - config_with( - build_dir="/path/to/build", - doctree_dir="${buildDir}/dts", - ), - config_with( - build_dir=pathlib.Path("/path/to/build/html"), - doctree_dir=pathlib.Path("/path/to/build/dts").resolve(), - ), - ), - # doctreeDir handling (non make mode) - ( - config_with( - build_dir="/path/to/build", doctree_dir=None, make_mode=False - ), - config_with( - build_dir=pathlib.Path("/path/to/build"), - doctree_dir=pathlib.Path("/path/to/build/.doctrees"), - make_mode=False, - ), - ), - ( - config_with( - build_dir="/path/to/build", - doctree_dir="/path/to/doctrees", - make_mode=False, - ), - config_with( - build_dir=pathlib.Path("/path/to/build"), - doctree_dir=pathlib.Path("/path/to/doctrees"), - make_mode=False, - ), - ), - ( - config_with( - build_dir="/path/to/build", - doctree_dir="${workspaceRoot}/doctrees", - make_mode=False, - ), - config_with( - build_dir=pathlib.Path("/path/to/build"), - doctree_dir=pathlib.Path("/path/to/root/doctrees").resolve(), - make_mode=False, - ), - ), - ( - config_with( - build_dir="/path/to/build", - doctree_dir="${workspaceFolder}/../dts", - make_mode=False, - ), - config_with( - build_dir=pathlib.Path("/path/to/build"), - doctree_dir=pathlib.Path("/path/to/dts").resolve(), - make_mode=False, - ), - ), - ( - config_with( - build_dir="/path/to/build", - doctree_dir="${confDir}/../dts", - make_mode=False, - ), - config_with( - build_dir=pathlib.Path("/path/to/build"), - doctree_dir=pathlib.Path("/path/to/dts").resolve(), - make_mode=False, - ), - ), - ( - config_with( - build_dir="/path/to/build", - doctree_dir="${buildDir}/dts", - make_mode=False, - ), - config_with( - build_dir=pathlib.Path("/path/to/build"), - doctree_dir=pathlib.Path("/path/to/build/dts").resolve(), - make_mode=False, - ), - ), - # srcDir handling - ( - config_with(src_dir="/path/to/src"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - src_dir=pathlib.Path("/path/to/src"), - ), - ), - ( - config_with(src_dir="~/path/to/src"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - src_dir=pathlib.Path("~/path/to/src").expanduser(), - ), - ), - ( - config_with(src_dir="${workspaceRoot}/src"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - src_dir=pathlib.Path("/path/to/root/src").resolve(), - ), - ), - ( - config_with(src_dir="${workspaceRoot}/../src"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - src_dir=pathlib.Path("/path/to/src").resolve(), - ), - ), - ( - config_with(src_dir="${workspaceFolder}/src"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - src_dir=pathlib.Path("/path/to/root/src").resolve(), - ), - ), - ( - config_with(src_dir="${workspaceFolder}/../src"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - src_dir=pathlib.Path("/path/to/src").resolve(), - ), - ), - ( - config_with(src_dir="${confDir}/src"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - src_dir=pathlib.Path("/path/to/conf/src").resolve(), - ), - ), - ( - config_with(src_dir="${confDir}/../src"), - config_with( - build_dir=pathlib.Path("/path/to/build/html").resolve(), - src_dir=pathlib.Path("/path/to/src").resolve(), - ), - ), - ], - ) - ], -) -def test_resolve(root_uri, setup: Tuple[SphinxConfig, SphinxConfig]): - """Ensure that we can resolve a config relative to a project root correctly.""" - config, expected = setup - actual = config.resolve(root_uri) - - # This seems hacky, but paths on windows are case insensitive... - if IS_WIN: - assert expected.build_dir.lower() == actual.build_dir.lower() - assert expected.conf_dir.lower() == actual.conf_dir.lower() - assert expected.doctree_dir.lower() == actual.doctree_dir.lower() - assert expected.src_dir.lower() == actual.src_dir.lower() - - else: - assert expected.build_dir == actual.build_dir - assert expected.conf_dir == actual.conf_dir - assert expected.doctree_dir == actual.doctree_dir - assert expected.src_dir == actual.src_dir - - assert expected.builder_name == actual.builder_name - assert expected.config_overrides == actual.config_overrides - assert expected.force_full_build == actual.force_full_build - assert expected.keep_going == actual.keep_going - assert expected.make_mode == actual.make_mode - assert expected.num_jobs == actual.num_jobs - assert expected.quiet == actual.quiet - assert expected.silent == actual.silent - assert expected.tags == actual.tags - assert expected.verbosity == actual.verbosity - assert expected.warning_is_error == actual.warning_is_error - - -@pytest.mark.parametrize( - "args", - [ - ["-M", "html", "src", "out"], - ["-M", "latex", "src", "out"], - ["-M", "html", "src", "out", "-E"], - ["-M", "html", "src", "out", "-c", "conf"], - ["-M", "html", "src", "out", "-d", "doctreedir"], - ["-M", "html", "src", "out", "-Dkey=value", "-Danother=v"], - ["-M", "html", "src", "out", "-Akey=value", "-Aanother=v"], - ["-M", "html", "src", "out", "-j", "4"], - ["-M", "html", "src", "out", "-n"], - ["-M", "html", "src", "out", "-q"], - ["-M", "html", "src", "out", "-Q"], - ["-M", "html", "src", "out", "-t", "tag1"], - ["-M", "html", "src", "out", "-t", "tag1", "-t", "tag2"], - ["-M", "html", "src", "out", "-v"], - ["-M", "html", "src", "out", "-vv"], - ["-M", "html", "src", "out", "-vvv"], - ["-M", "html", "src", "out", "-W"], - ["-M", "html", "src", "out", "-W", "--keep-going"], - ["-b", "html", "src", "out"], - ["-b", "latex", "src", "out"], - ["-b", "html", "-E", "src", "out"], - ["-b", "html", "-c", "conf", "src", "out"], - ["-b", "html", "-Dkey=value", "-Danother=v", "src", "out"], - ["-b", "html", "-Akey=value", "-Aanother=v", "src", "out"], - ["-b", "html", "-d", "doctreedir", "src", "out"], - ["-b", "html", "-j", "4", "src", "out"], - ["-b", "html", "-n", "src", "out"], - ["-b", "html", "-q", "src", "out"], - ["-b", "html", "-Q", "src", "out"], - ["-b", "html", "-t", "tag1", "src", "out"], - ["-b", "html", "-t", "tag1", "-t", "tag2", "src", "out"], - ["-b", "html", "-v", "src", "out"], - ["-b", "html", "-vv", "src", "out"], - ["-b", "html", "-vvv", "src", "out"], - ["-b", "html", "-W", "src", "out"], - ["-b", "html", "-W", "--keep-going", "src", "out"], - ], -) -def test_cli_arg_handling(args: List[str]): - """Ensure that we can convert ``sphinx-build`` to initialization options and back.""" - - config = SphinxConfig.from_arguments(cli_args=args) - actual = config.to_cli_args() - - assert args == actual - - -ROOT = pathlib.Path(__file__).parent.parent / "sphinx-extensions" / "workspace" -PY_PATH = ROOT / "code" / "diagnostics.py" -CONF_PATH = ROOT / "sphinx-extensions" / "conf.py" -RST_PATH = ROOT / "sphinx-extensions" / "index.rst" -INC_PATH = ROOT / "sphinx-extensions" / "_include_me.txt" -REL_INC_PATH = os.path.relpath(INC_PATH) - - -@pytest.mark.parametrize( - "location, expected", - [ - ("", (uri.from_fs_path(str(CONF_PATH)), None)), - (f"{RST_PATH}", (uri.from_fs_path(str(RST_PATH)), None)), - (f"{RST_PATH}:", (uri.from_fs_path(str(RST_PATH)), None)), - (f"{RST_PATH}:3", (uri.from_fs_path(str(RST_PATH)), 3)), - (f"{REL_INC_PATH}:12", (uri.from_fs_path(str(INC_PATH)), 12)), - ( - f"{PY_PATH}:docstring of esbonio.lsp.sphinx.config.SphinxLogHandler:3", - (uri.from_fs_path(str(PY_PATH)), 22), - ), - ( - f"internal padding after {RST_PATH}:34", - (uri.from_fs_path(str(RST_PATH)), 34), - ), - ( - f"internal padding before {RST_PATH}:34", - (uri.from_fs_path(str(RST_PATH)), 34), - ), - ], -) -def test_get_diagnostic_location(location: str, expected: Tuple[str, Optional[int]]): - """Ensure we can correctly determine a dianostic's location based on the string we - get from sphinx.""" - - app = mock.Mock() - app.confdir = str(ROOT / "sphinx-extensions") - - server = mock.Mock() - handler = SphinxLogHandler(app, server) - - mockpath = f"{SphinxLogHandler.__module__}.inspect.getsourcelines" - with mock.patch(mockpath, return_value=([""], 20)): - actual = handler.get_location(location) - - assert actual == expected diff --git a/lib/esbonio/tests/workspaces/demo/conf.py b/lib/esbonio/tests/workspaces/demo/conf.py index 6f2423598..bbd74a0ee 100644 --- a/lib/esbonio/tests/workspaces/demo/conf.py +++ b/lib/esbonio/tests/workspaces/demo/conf.py @@ -35,7 +35,6 @@ "source_branch": "develop", "source_directory": "lib/esbonio/tests/workspaces/demo/", } -html_static_path = ["_static"] def lsp_role(name, rawtext, text, lineno, inliner, options={}, content=[]): diff --git a/lib/esbonio/tests/workspaces/demo/myst/diagnostics.md b/lib/esbonio/tests/workspaces/demo/myst/diagnostics.md new file mode 100644 index 000000000..d12a10ced --- /dev/null +++ b/lib/esbonio/tests/workspaces/demo/myst/diagnostics.md @@ -0,0 +1,6 @@ +# Diagnostics + +The language server has support for diagnostics, highlighting errors/warnings reported by Sphinx. + +```{image} /not-an-image.png +``` diff --git a/lib/esbonio/tests/workspaces/demo/rst/diagnostics.rst b/lib/esbonio/tests/workspaces/demo/rst/diagnostics.rst new file mode 100644 index 000000000..0f9bf45e3 --- /dev/null +++ b/lib/esbonio/tests/workspaces/demo/rst/diagnostics.rst @@ -0,0 +1,6 @@ +Diagnostics +=========== + +The language server has support for diagnostics, highlighting errors/warnings reported by Sphinx. + +.. image:: /not-an-image.png diff --git a/lib/esbonio/tox.ini b/lib/esbonio/tox.ini index 8c54db1cb..361150496 100644 --- a/lib/esbonio/tox.ini +++ b/lib/esbonio/tox.ini @@ -9,6 +9,9 @@ description = "Run the test suite for the server component of esbonio" package = wheel wheel_build_env = .pkg deps = + + py38: importlib-resources>6,<6.2 + coverage[toml] pytest pytest-lsp>=0.3.1 diff --git a/scripts/check-sphinx-version.py b/scripts/check-sphinx-version.py index 2861a482a..f4b0f18a0 100644 --- a/scripts/check-sphinx-version.py +++ b/scripts/check-sphinx-version.py @@ -1,4 +1,5 @@ """Check that we are testing the correct Sphinx version inside tox.""" + import os import sphinx diff --git a/scripts/generate_docutils_documentation.py b/scripts/generate_docutils_documentation.py index 1f6e0ebd0..419250075 100644 --- a/scripts/generate_docutils_documentation.py +++ b/scripts/generate_docutils_documentation.py @@ -1,5 +1,6 @@ """Script to convert documentation for docutils' roles and directives into a format the language server can use.""" + import argparse import importlib import json diff --git a/scripts/preview_documentation.py b/scripts/preview_documentation.py index 4709ce698..1daf7b3c9 100644 --- a/scripts/preview_documentation.py +++ b/scripts/preview_documentation.py @@ -1,4 +1,5 @@ """Script to preview completion item documentation.""" + import argparse import json import re diff --git a/scripts/sphinx-app.py b/scripts/sphinx-app.py index eaf0954ab..753cba21d 100644 --- a/scripts/sphinx-app.py +++ b/scripts/sphinx-app.py @@ -29,21 +29,27 @@ >>> app <sphinx.application.Sphinx object at 0x7f0ad0fdb5b0> """ + import inspect import pathlib +import pdb import time from esbonio.sphinx_agent import types from esbonio.sphinx_agent.app import Sphinx -root = pathlib.Path(__file__).parent.parent - -app = Sphinx( - srcdir=root / "docs", - confdir=root / "docs", - outdir=root / "docs" / "_build", - doctreedir=root / "docs" / "_build" / "doctrees", - buildername="html", - freshenv=True, # Have Sphinx reload everything on first build. -) -app.build() +try: + root = pathlib.Path(__file__).parent.parent + # project = root / "docs" + project = root / "lib" / "esbonio" / "tests" / "workspaces" / "demo" + app = Sphinx( + srcdir=project, + confdir=project, + outdir=project / "_build", + doctreedir=project / "_build" / "doctrees", + buildername="html", + freshenv=True, # Have Sphinx reload everything on first build. + ) + app.build() +except Exception: + pdb.post_mortem()