diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..901cad098 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +FROM mcr.microsoft.com/devcontainers/base:jammy + +COPY tools.mk / + +RUN apt-get update \ + && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends python3-venv \ + && su vscode -c "make -f tools.mk tools" \ + && rm tools.mk diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..aac2d51f9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu +{ + "name": "Ubuntu", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "ghcr.io/swyddfa/esbonio-devenv:latest", + // "build": { + // "dockerfile": "Dockerfile" + // }, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "charliermarsh.ruff", + "ms-python.python", + "tamasfe.even-better-toml" + ] + } + } + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.devcontainer/tools.mk b/.devcontainer/tools.mk new file mode 100644 index 000000000..4b0c4cc0a --- /dev/null +++ b/.devcontainer/tools.mk @@ -0,0 +1,143 @@ +ARCH ?= $(shell arch) +BIN ?= $(HOME)/.local/bin + +ifeq ($(strip $(ARCH)),) +$(error Unable to determine platform architecture) +endif + +HATCH_VERSION = 1.10.0 +NODE_VERSION := 18.20.2 + +# The versions of Python we support +PYXX_versions := 3.8 3.9 3.10 3.11 3.12 +PY_INTERPRETERS = + +# Hatch is not only used for building packages, but bootstrapping any missing +# interpreters +HATCH ?= $(or $(shell command -v hatch), $(BIN)/hatch) + +$(HATCH): + curl -L --output /tmp/hatch.tar.gz https://github.com/pypa/hatch/releases/download/hatch-v$(HATCH_VERSION)/hatch-$(HATCH_VERSION)-$(ARCH)-unknown-linux-gnu.tar.gz + tar -xf /tmp/hatch.tar.gz -C /tmp + rm /tmp/hatch.tar.gz + + test -d $(BIN) || mkdir -p $(BIN) + mv /tmp/hatch-$(HATCH_VERSION)-$(ARCH)-unknown-linux-gnu $(HATCH) + + $@ --version + touch $@ + +# This effectively defines a function `PYXX` that takes a Python version number +# (e.g. 3.8) and expands it out into a common block of code that will ensure a +# verison of that interpreter is available to be used. +# +# The is perhaps a bit more complicated than I'd like, but it should mean that +# the project's makefiles are useful both inside and outside of a devcontainer. +# +# `PYXX` has the following behavior: +# - If possible, it will reuse the user's existing version of Python +# i.e. $(shell command -v pythonX.X) +# +# - The user may force a specific interpreter to be used by setting the +# variable when running make e.g. PYXX=/path/to/pythonX.X make ... +# +# - Otherwise, `make` will use `$(HATCH)` to install the given version of +# Python under `$(BIN)` +# +# See: https://www.gnu.org/software/make/manual/html_node/Eval-Function.html +define PYXX = + +PY$(subst .,,$1) ?= $$(shell command -v python$1) + +ifeq ($$(strip $$(PY$(subst .,,$1))),) + +PY$(subst .,,$1) := $$(BIN)/python$1 + +$$(PY$(subst .,,$1)): $$(HATCH) + $$(HATCH) python find $1 || $$(HATCH) python install $1 + ln -s $$$$($$(HATCH) python find $1) $$@ + + $$@ --version + touch $$@ + +endif + +PY_INTERPRETERS += $$(PY$(subst .,,$1)) +endef + +# Uncomment the following line to see what this expands into. +#$(foreach version,$(PYXX_versions),$(info $(call PYXX,$(version)))) +$(foreach version,$(PYXX_versions),$(eval $(call PYXX,$(version)))) + +# Set a default `python` command if there is not one already +PY ?= $(shell command -v python3) + +ifeq ($(strip $(PY)),) +PY := $(BIN)/python + +$(PY): $(PY312) + ln -s $< $@ + $@ --version + touch $@ +endif + +PY_INTERPRETERS += $(PY) +#$(info $(PY_INTERPRETERS)) + +PIPX ?= $(shell command -v pipx) + +ifeq ($(strip $(PIPX)),) +PIPX := $(BIN)/pipx +PIPX_VERSION := 1.5.0 + +$(PIPX): + curl -L -o $(BIN)/pipx.pyz https://github.com/pypa/pipx/releases/download/$(PIPX_VERSION)/pipx.pyz + echo '#!/bin/bash\nexec $(PY) $(BIN)/pipx.pyz "$$@"' > $(PIPX) + + chmod +x $(PIPX) + $@ --version + touch $@ +endif + +PRE_COMMIT ?= $(shell command -v pre-commit) + +ifeq ($(strip $(PRE_COMMIT)),) +PRE_COMMIT := $(BIN)/pre-commit + +$(PRE_COMMIT): $(PIPX) + $(PIPX) install pre-commit + $@ --version + touch $@ +endif + +PY_TOOLS := $(HATCH) $(PIPX) $(PRE_COMMIT) + +# Node JS +NPM ?= $(shell command -v npm) + +ifeq ($(strip $(NPM)),) + +NPM := $(BIN)/npm +NODE := $(BIN)/node +NODE_DIR := $(HOME)/.local/node + +$(NPM): + curl -L --output /tmp/node.tar.xz https://nodejs.org/dist/v$(NODE_VERSION)/node-v$(NODE_VERSION)-linux-x64.tar.xz + tar -xJf /tmp/node.tar.xz -C /tmp + rm /tmp/node.tar.xz + + [ -d $(NODE_DIR) ] || mkdir -p $(NODE_DIR) + mv /tmp/node-v$(NODE_VERSION)-linux-x64/* $(NODE_DIR) + + [ -d $(BIN) ] || mkdir -p $(BIN) + ln -s $(NODE_DIR)/bin/node $(NODE) + ln -s $(NODE_DIR)/bin/npm $(NPM) + + $(NODE) --version + PATH=$(BIN) $(NPM) --version + +endif + +# One command to bootstrap all tools and check their versions +tools: $(PY_INTERPRETERS) $(PY_TOOLS) $(NPM) + for prog in $^ ; do echo -n "$${prog}\t" ; PATH=$(BIN) $${prog} --version; done diff --git a/.github/workflows/devenv.yml b/.github/workflows/devenv.yml new file mode 100644 index 000000000..3133a5c74 --- /dev/null +++ b/.github/workflows/devenv.yml @@ -0,0 +1,57 @@ +name: 'Build devcontainer' +on: + + workflow_dispatch: + + pull_request: + branches: + - develop + paths: + - '.devcontainer/**' + + push: + branches: + - develop + paths: + - '.devcontainer/**' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}-devenv + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v5 + with: + context: .devcontainer/ + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/lsp-pr.yml b/.github/workflows/lsp-pr.yml index 785c8cd50..cd5246a28 100644 --- a/.github/workflows/lsp-pr.yml +++ b/.github/workflows/lsp-pr.yml @@ -64,14 +64,11 @@ jobs: - run: | python --version - python -m pip install --upgrade pip - python -m pip install --upgrade tox + python -m pip install --upgrade hatch name: Setup Environment - run: | cd lib/esbonio - version=$(echo ${{ matrix.python-version }} | tr -d .) - python -m tox run -e `tox -l | grep $version | tr '\n' ','` - shell: bash + hatch test -i py=${{ matrix.python-version }} name: Run Tests diff --git a/.github/workflows/vscode-pr.yml b/.github/workflows/vscode-pr.yml index 1b3844683..bb51ddbb4 100644 --- a/.github/workflows/vscode-pr.yml +++ b/.github/workflows/vscode-pr.yml @@ -35,7 +35,7 @@ jobs: - run: | python --version python -m pip install --upgrade pip - python -m pip install --upgrade hatch tox towncrier 'importlib-resources<6' + python -m pip install --upgrade hatch towncrier name: Install Build Tools - run: | @@ -57,10 +57,7 @@ jobs: # Use in-repo version of esbonio for dev builds echo "whl=${ESBONIO_WHL}" - ESBONIO_WHL=${ESBONIO_WHL} tox run -e bundle-deps - - npm ci --prefer-offline - npm run package + ESBONIO=${ESBONIO_WHL} make dist id: assets name: Package Extension diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25e0683f6..d3c347e6a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,27 +8,16 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/psf/black - rev: 24.4.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.7 hooks: - - id: black + - id: ruff + args: [--fix] -- repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - exclude: 'scripts/sphinx-app.py' - args: [--config=lib/esbonio/setup.cfg] - -- repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - name: isort (python) - args: [--settings-file=lib/esbonio/pyproject.toml] + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.9.0' + rev: 'v1.10.0' hooks: - id: mypy name: mypy (scripts) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index bf6f24fcc..a65ea7964 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,7 @@ { "recommendations": [ + "charliermarsh.ruff", "ms-python.python", - "ms-python.black-formatter", - "ms-python.isort", - "swyddfa.esbonio" + "tamasfe.even-better-toml" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 42e8d7c8f..3bd2b184d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -36,10 +36,7 @@ "outFiles": [ "${workspaceRoot}/code/dist/node/**/*.js" ], - // "preLaunchTask": "${defaultBuildTask}", - "env": { - // "VSCODE_LSP_DEBUG": "true" - } + "preLaunchTask": "${defaultBuildTask}", }, { "name": "VSCode Web Extension", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 25fc86497..d0181bd9d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,9 +2,13 @@ "version": "2.0.0", "tasks": [ { - "type": "npm", - "script": "watch", + "label": "npm: watch", + "type": "process", "isBackground": true, + "command": "make", + "args": [ + "watch" + ], "options": { "cwd": "${workspaceRoot}/code" }, @@ -14,17 +18,10 @@ }, "presentation": { "panel": "dedicated", - "reveal": "never" + "reveal": "always" }, "problemMatcher": [ - { - "base": "$tsc-watch", - "background": { - "activeOnStart": true, - "beginsPattern": "asset .*", - "endsPattern": "webpack .* compiled .*" - } - } + "$tsc-watch" ] }, { diff --git a/Makefile b/Makefile index 7f92c86eb..dcb3caa25 100644 --- a/Makefile +++ b/Makefile @@ -1,53 +1,13 @@ -.PHONY: preview-completion-docs completion-docs venv npm +include .devcontainer/tools.mk -VENV := .env +.PHONY: lint enable-pre-commit disable-pre-commit -ifeq ($(CI),true) - PYTHON=python -endif +lint: $(PRE_COMMIT) + $(PRE_COMMIT) run --all-files -# Default python env. -ifndef PYTHON - PYTHON=$(VENV)/bin/python -endif -# ---------------------------------------- Development Environments ------------------------------------------- -$(VENV)/bin/python: - python3 -m venv $(VENV) - $@ -m pip install --upgrade pip - $@ -m pip install -r docs/requirements.txt - $@ -m pip install -e lib/esbonio[dev] - $@ -m pip install -e lib/esbonio-extensions[dev] +enable-pre-commit: $(PRE_COMMIT) + $(PRE_COMMIT) install -venv: $(VENV)/bin/python - -code/node_modules: code/package.json code/package-lock.json - npm --prefix ./code/ install - -npm: code/node_modules - -# ---------------------------------------- Tests, Lints, Tools etc. ------------------------------------------- -mypy: $(PYTHON) - mypy --namespace-packages --explicit-package-bases -p esbonio - -# ---------------------------------------- CompletionItem Documentation --------------------------------------- -DOCUTILS_COMPLETION_DOCS=lib/esbonio/esbonio/lsp/rst/roles.json lib/esbonio/esbonio/lsp/rst/directives.json -SPHINX_COMPLETION_DOCS=lib/esbonio/esbonio/lsp/sphinx/roles.json lib/esbonio/esbonio/lsp/sphinx/directives.json -COMPLETION_DOCS=$(SPHINX_COMPLETION_DOCS) $(DOCUTILS_COMPLETION_DOCS) - -# '&:' Indicates that the multiple targets listed are 'grouped' and that they are produced together from running -# the command below once. This prevents the command being run once for each file produced. -# https://www.gnu.org/software/make/manual/html_node/Multiple-Targets.html -# -# '$^' Expands into the list of dependencies of the target (python .... in this case) -# https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html#Automatic-Variables -$(DOCUTILS_COMPLETION_DOCS) &: $(PYTHON) scripts/generate_docutils_documentation.py - $^ -o lib/esbonio/esbonio/lsp/rst/ - -$(SPHINX_COMPLETION_DOCS) &: $(PYTHON) scripts/generate_sphinx_documentation.py - $^ -o lib/esbonio/esbonio/lsp/sphinx/ - -completion-docs: $(COMPLETION_DOCS) - -preview-completion-docs: $(COMPLETION_DOCS) - $(PYTHON) scripts/preview_documentation.py $(COMPLETION_DOCS) +disable-pre-commit: $(PRE_COMMIT) + $(PRE_COMMIT) uninstall diff --git a/code/Makefile b/code/Makefile new file mode 100644 index 000000000..227255abb --- /dev/null +++ b/code/Makefile @@ -0,0 +1,48 @@ +include ../.devcontainer/tools.mk + +ESBONIO ?= --pre esbonio + +.PHONY: dist dev-deps release-deps release + +watch: dev-deps $(NPM) + -test -d dist && rm -r dist + $(NPM) run watch + +compile: dev-deps $(NPM) + -test -d dist && rm -r dist + $(NPM) run compile + +dist: release-deps $(NPM) + -test -d dist && rm -r dist + $(NPM) run package + +release: $(TOWNCRIER) $(HATCH) $(PY) + $(PY) ../scripts/make_release.py lsp + $(PY) ../scripts/make_release.py vscode + +# Ensures the version of esbonio in ../lib/esbonio is used. +dev-deps: node_modules/.installed bundled/libs/.installed + -test -d bundled/libs/esbonio-*.dist-info && rm -r bundled/libs/esbonio-*.dist-info + -test -L bundled/libs/esbonio || rm -r bundled/libs/esbonio + if [ ! -f bundled/libs/esbonio/__main__.py ]; then \ + test -L bundled/libs/esbonio && rm bundled/libs/esbonio; \ + ln -s $(shell pwd)/../lib/esbonio/esbonio bundled/libs/esbonio; \ + fi + +# Ensures the latest version of esbonio from PyPi is used. +release-deps: node_modules/.installed bundled/libs/.installed + -test -L bundled/libs/esbonio && rm bundled/libs/esbonio + test -d bundled/libs/esbonio-*.dist-info || $(PY38) -m pip install -t ./bundled/libs --no-cache-dir --implementation py --no-deps --upgrade $(ESBONIO) + +requirements.txt: $(HATCH) requirements.in + $(HATCH) run deps:update + +bundled/libs/.installed: $(PY38) requirements.txt + -test -d bundled/libs && rm -r bundled/libs + $(PY38) --version + $(PY38) -m pip install -t ./bundled/libs --no-cache-dir --implementation py --no-deps --upgrade -r ./requirements.txt + touch $@ + +node_modules/.installed: package.json package-lock.json $(NPM) + $(NPM) ci + touch $@ diff --git a/code/hatch.toml b/code/hatch.toml new file mode 100644 index 000000000..df070ec3e --- /dev/null +++ b/code/hatch.toml @@ -0,0 +1,10 @@ +[envs.deps] +python = "3.8" +dependencies = ["pip-tools"] +skip-install = true + +[envs.deps.scripts] +update = [ + "python --version", + "pip-compile --resolver=backtracking --generate-hashes --upgrade requirements.in", +] diff --git a/code/package-lock.json b/code/package-lock.json index 83dffd4df..a6d203170 100644 --- a/code/package-lock.json +++ b/code/package-lock.json @@ -1,24 +1,24 @@ { "name": "esbonio", - "version": "0.93.1", + "version": "0.94.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "esbonio", - "version": "0.93.1", + "version": "0.94.0", "license": "MIT", "dependencies": { "@vscode/python-extension": "^1.0.5", - "semver": "^7.6.0", + "semver": "^7.6.2", "vscode-languageclient": "^9.0.1" }, "devDependencies": { "@types/glob": "^8.1.0", "@types/node": "^18", "@types/vscode": "1.78.0", - "@vscode/vsce": "^2.26.0", - "esbuild": "^0.20.2", + "@vscode/vsce": "^2.26.1", + "esbuild": "^0.21.4", "ovsx": "^0.9.1", "typescript": "^5.4.5" }, @@ -235,9 +235,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.4.tgz", + "integrity": "sha512-Zrm+B33R4LWPLjDEVnEqt2+SLTATlru1q/xYKVn8oVTbiRBGmK2VIMoIYGJDGyftnGaC788IuzGFAlb7IQ0Y8A==", "cpu": [ "ppc64" ], @@ -251,9 +251,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.4.tgz", + "integrity": "sha512-E7H/yTd8kGQfY4z9t3nRPk/hrhaCajfA3YSQSBrst8B+3uTcgsi8N+ZWYCaeIDsiVs6m65JPCaQN/DxBRclF3A==", "cpu": [ "arm" ], @@ -267,9 +267,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.4.tgz", + "integrity": "sha512-fYFnz+ObClJ3dNiITySBUx+oNalYUT18/AryMxfovLkYWbutXsct3Wz2ZWAcGGppp+RVVX5FiXeLYGi97umisA==", "cpu": [ "arm64" ], @@ -283,9 +283,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.4.tgz", + "integrity": "sha512-mDqmlge3hFbEPbCWxp4fM6hqq7aZfLEHZAKGP9viq9wMUBVQx202aDIfc3l+d2cKhUJM741VrCXEzRFhPDKH3Q==", "cpu": [ "x64" ], @@ -299,9 +299,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "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==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.4.tgz", + "integrity": "sha512-72eaIrDZDSiWqpmCzVaBD58c8ea8cw/U0fq/PPOTqE3c53D0xVMRt2ooIABZ6/wj99Y+h4ksT/+I+srCDLU9TA==", "cpu": [ "arm64" ], @@ -315,9 +315,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.4.tgz", + "integrity": "sha512-uBsuwRMehGmw1JC7Vecu/upOjTsMhgahmDkWhGLWxIgUn2x/Y4tIwUZngsmVb6XyPSTXJYS4YiASKPcm9Zitag==", "cpu": [ "x64" ], @@ -331,9 +331,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.4.tgz", + "integrity": "sha512-8JfuSC6YMSAEIZIWNL3GtdUT5NhUA/CMUCpZdDRolUXNAXEE/Vbpe6qlGLpfThtY5NwXq8Hi4nJy4YfPh+TwAg==", "cpu": [ "arm64" ], @@ -347,9 +347,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.4.tgz", + "integrity": "sha512-8d9y9eQhxv4ef7JmXny7591P/PYsDFc4+STaxC1GBv0tMyCdyWfXu2jBuqRsyhY8uL2HU8uPyscgE2KxCY9imQ==", "cpu": [ "x64" ], @@ -363,9 +363,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.4.tgz", + "integrity": "sha512-2rqFFefpYmpMs+FWjkzSgXg5vViocqpq5a1PSRgT0AvSgxoXmGF17qfGAzKedg6wAwyM7UltrKVo9kxaJLMF/g==", "cpu": [ "arm" ], @@ -379,9 +379,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "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==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.4.tgz", + "integrity": "sha512-/GLD2orjNU50v9PcxNpYZi+y8dJ7e7/LhQukN3S4jNDXCKkyyiyAz9zDw3siZ7Eh1tRcnCHAo/WcqKMzmi4eMQ==", "cpu": [ "arm64" ], @@ -395,9 +395,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.4.tgz", + "integrity": "sha512-pNftBl7m/tFG3t2m/tSjuYeWIffzwAZT9m08+9DPLizxVOsUl8DdFzn9HvJrTQwe3wvJnwTdl92AonY36w/25g==", "cpu": [ "ia32" ], @@ -411,9 +411,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.4.tgz", + "integrity": "sha512-cSD2gzCK5LuVX+hszzXQzlWya6c7hilO71L9h4KHwqI4qeqZ57bAtkgcC2YioXjsbfAv4lPn3qe3b00Zt+jIfQ==", "cpu": [ "loong64" ], @@ -427,9 +427,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.4.tgz", + "integrity": "sha512-qtzAd3BJh7UdbiXCrg6npWLYU0YpufsV9XlufKhMhYMJGJCdfX/G6+PNd0+v877X1JG5VmjBLUiFB0o8EUSicA==", "cpu": [ "mips64el" ], @@ -443,9 +443,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.4.tgz", + "integrity": "sha512-yB8AYzOTaL0D5+2a4xEy7OVvbcypvDR05MsB/VVPVA7nL4hc5w5Dyd/ddnayStDgJE59fAgNEOdLhBxjfx5+dg==", "cpu": [ "ppc64" ], @@ -459,9 +459,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.4.tgz", + "integrity": "sha512-Y5AgOuVzPjQdgU59ramLoqSSiXddu7F3F+LI5hYy/d1UHN7K5oLzYBDZe23QmQJ9PIVUXwOdKJ/jZahPdxzm9w==", "cpu": [ "riscv64" ], @@ -475,9 +475,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.4.tgz", + "integrity": "sha512-Iqc/l/FFwtt8FoTK9riYv9zQNms7B8u+vAI/rxKuN10HgQIXaPzKZc479lZ0x6+vKVQbu55GdpYpeNWzjOhgbA==", "cpu": [ "s390x" ], @@ -491,9 +491,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.4.tgz", + "integrity": "sha512-Td9jv782UMAFsuLZINfUpoF5mZIbAj+jv1YVtE58rFtfvoKRiKSkRGQfHTgKamLVT/fO7203bHa3wU122V/Bdg==", "cpu": [ "x64" ], @@ -507,9 +507,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.4.tgz", + "integrity": "sha512-Awn38oSXxsPMQxaV0Ipb7W/gxZtk5Tx3+W+rAPdZkyEhQ6968r9NvtkjhnhbEgWXYbgV+JEONJ6PcdBS+nlcpA==", "cpu": [ "x64" ], @@ -523,9 +523,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.4.tgz", + "integrity": "sha512-IsUmQeCY0aU374R82fxIPu6vkOybWIMc3hVGZ3ChRwL9hA1TwY+tS0lgFWV5+F1+1ssuvvXt3HFqe8roCip8Hg==", "cpu": [ "x64" ], @@ -539,9 +539,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.4.tgz", + "integrity": "sha512-hsKhgZ4teLUaDA6FG/QIu2q0rI6I36tZVfM4DBZv3BG0mkMIdEnMbhc4xwLvLJSS22uWmaVkFkqWgIS0gPIm+A==", "cpu": [ "x64" ], @@ -555,9 +555,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.4.tgz", + "integrity": "sha512-UUfMgMoXPoA/bvGUNfUBFLCh0gt9dxZYIx9W4rfJr7+hKe5jxxHmfOK8YSH4qsHLLN4Ck8JZ+v7Q5fIm1huErg==", "cpu": [ "arm64" ], @@ -571,9 +571,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.4.tgz", + "integrity": "sha512-yIxbspZb5kGCAHWm8dexALQ9en1IYDfErzjSEq1KzXFniHv019VT3mNtTK7t8qdy4TwT6QYHI9sEZabONHg+aw==", "cpu": [ "ia32" ], @@ -587,9 +587,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.4.tgz", + "integrity": "sha512-sywLRD3UK/qRJt0oBwdpYLBibk7KiRfbswmWRDabuncQYSlf8aLEEUor/oP6KRz8KEG+HoiVLBhPRD5JWjS8Sg==", "cpu": [ "x64" ], @@ -640,9 +640,9 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.26.0.tgz", - "integrity": "sha512-v54ltgMzUG8lGY0kAgaOlry57xse1RlWzes9FotfGEx+Fr05KeR8rZicQzEMDmi9QnOgVWHuiEq+xA2HWkAz+Q==", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.26.1.tgz", + "integrity": "sha512-QOG6Ht7V93nhwcBxPWcG33UK0qDGEoJdg0xtVeaTN27W6PGdMJUJGTPhB/sNHUIFKwvwzv/zMAHvDgMNXbcwlA==", "dev": true, "dependencies": { "@azure/identity": "^4.1.0", @@ -1248,9 +1248,9 @@ } }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.4.tgz", + "integrity": "sha512-sFMcNNrj+Q0ZDolrp5pDhH0nRPN9hLIM3fRPwgbLYJeSHHgnXSnbV3xYgSVuOeLWH9c73VwmEverVzupIv5xuA==", "dev": true, "hasInstallScript": true, "bin": { @@ -1260,29 +1260,29 @@ "node": ">=12" }, "optionalDependencies": { - "@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" + "@esbuild/aix-ppc64": "0.21.4", + "@esbuild/android-arm": "0.21.4", + "@esbuild/android-arm64": "0.21.4", + "@esbuild/android-x64": "0.21.4", + "@esbuild/darwin-arm64": "0.21.4", + "@esbuild/darwin-x64": "0.21.4", + "@esbuild/freebsd-arm64": "0.21.4", + "@esbuild/freebsd-x64": "0.21.4", + "@esbuild/linux-arm": "0.21.4", + "@esbuild/linux-arm64": "0.21.4", + "@esbuild/linux-ia32": "0.21.4", + "@esbuild/linux-loong64": "0.21.4", + "@esbuild/linux-mips64el": "0.21.4", + "@esbuild/linux-ppc64": "0.21.4", + "@esbuild/linux-riscv64": "0.21.4", + "@esbuild/linux-s390x": "0.21.4", + "@esbuild/linux-x64": "0.21.4", + "@esbuild/netbsd-x64": "0.21.4", + "@esbuild/openbsd-x64": "0.21.4", + "@esbuild/sunos-x64": "0.21.4", + "@esbuild/win32-arm64": "0.21.4", + "@esbuild/win32-ia32": "0.21.4", + "@esbuild/win32-x64": "0.21.4" } }, "node_modules/escape-string-regexp": { @@ -1871,6 +1871,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -2333,12 +2334,9 @@ "dev": true }, "node_modules/semver": { - "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" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -2746,7 +2744,8 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/yauzl": { "version": "2.10.0", @@ -2941,163 +2940,163 @@ } }, "@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.4.tgz", + "integrity": "sha512-Zrm+B33R4LWPLjDEVnEqt2+SLTATlru1q/xYKVn8oVTbiRBGmK2VIMoIYGJDGyftnGaC788IuzGFAlb7IQ0Y8A==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.4.tgz", + "integrity": "sha512-E7H/yTd8kGQfY4z9t3nRPk/hrhaCajfA3YSQSBrst8B+3uTcgsi8N+ZWYCaeIDsiVs6m65JPCaQN/DxBRclF3A==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.4.tgz", + "integrity": "sha512-fYFnz+ObClJ3dNiITySBUx+oNalYUT18/AryMxfovLkYWbutXsct3Wz2ZWAcGGppp+RVVX5FiXeLYGi97umisA==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.4.tgz", + "integrity": "sha512-mDqmlge3hFbEPbCWxp4fM6hqq7aZfLEHZAKGP9viq9wMUBVQx202aDIfc3l+d2cKhUJM741VrCXEzRFhPDKH3Q==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "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==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.4.tgz", + "integrity": "sha512-72eaIrDZDSiWqpmCzVaBD58c8ea8cw/U0fq/PPOTqE3c53D0xVMRt2ooIABZ6/wj99Y+h4ksT/+I+srCDLU9TA==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.4.tgz", + "integrity": "sha512-uBsuwRMehGmw1JC7Vecu/upOjTsMhgahmDkWhGLWxIgUn2x/Y4tIwUZngsmVb6XyPSTXJYS4YiASKPcm9Zitag==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.4.tgz", + "integrity": "sha512-8JfuSC6YMSAEIZIWNL3GtdUT5NhUA/CMUCpZdDRolUXNAXEE/Vbpe6qlGLpfThtY5NwXq8Hi4nJy4YfPh+TwAg==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.4.tgz", + "integrity": "sha512-8d9y9eQhxv4ef7JmXny7591P/PYsDFc4+STaxC1GBv0tMyCdyWfXu2jBuqRsyhY8uL2HU8uPyscgE2KxCY9imQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.4.tgz", + "integrity": "sha512-2rqFFefpYmpMs+FWjkzSgXg5vViocqpq5a1PSRgT0AvSgxoXmGF17qfGAzKedg6wAwyM7UltrKVo9kxaJLMF/g==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "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==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.4.tgz", + "integrity": "sha512-/GLD2orjNU50v9PcxNpYZi+y8dJ7e7/LhQukN3S4jNDXCKkyyiyAz9zDw3siZ7Eh1tRcnCHAo/WcqKMzmi4eMQ==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.4.tgz", + "integrity": "sha512-pNftBl7m/tFG3t2m/tSjuYeWIffzwAZT9m08+9DPLizxVOsUl8DdFzn9HvJrTQwe3wvJnwTdl92AonY36w/25g==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.4.tgz", + "integrity": "sha512-cSD2gzCK5LuVX+hszzXQzlWya6c7hilO71L9h4KHwqI4qeqZ57bAtkgcC2YioXjsbfAv4lPn3qe3b00Zt+jIfQ==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.4.tgz", + "integrity": "sha512-qtzAd3BJh7UdbiXCrg6npWLYU0YpufsV9XlufKhMhYMJGJCdfX/G6+PNd0+v877X1JG5VmjBLUiFB0o8EUSicA==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.4.tgz", + "integrity": "sha512-yB8AYzOTaL0D5+2a4xEy7OVvbcypvDR05MsB/VVPVA7nL4hc5w5Dyd/ddnayStDgJE59fAgNEOdLhBxjfx5+dg==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.4.tgz", + "integrity": "sha512-Y5AgOuVzPjQdgU59ramLoqSSiXddu7F3F+LI5hYy/d1UHN7K5oLzYBDZe23QmQJ9PIVUXwOdKJ/jZahPdxzm9w==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.4.tgz", + "integrity": "sha512-Iqc/l/FFwtt8FoTK9riYv9zQNms7B8u+vAI/rxKuN10HgQIXaPzKZc479lZ0x6+vKVQbu55GdpYpeNWzjOhgbA==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.4.tgz", + "integrity": "sha512-Td9jv782UMAFsuLZINfUpoF5mZIbAj+jv1YVtE58rFtfvoKRiKSkRGQfHTgKamLVT/fO7203bHa3wU122V/Bdg==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.4.tgz", + "integrity": "sha512-Awn38oSXxsPMQxaV0Ipb7W/gxZtk5Tx3+W+rAPdZkyEhQ6968r9NvtkjhnhbEgWXYbgV+JEONJ6PcdBS+nlcpA==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.4.tgz", + "integrity": "sha512-IsUmQeCY0aU374R82fxIPu6vkOybWIMc3hVGZ3ChRwL9hA1TwY+tS0lgFWV5+F1+1ssuvvXt3HFqe8roCip8Hg==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.4.tgz", + "integrity": "sha512-hsKhgZ4teLUaDA6FG/QIu2q0rI6I36tZVfM4DBZv3BG0mkMIdEnMbhc4xwLvLJSS22uWmaVkFkqWgIS0gPIm+A==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.4.tgz", + "integrity": "sha512-UUfMgMoXPoA/bvGUNfUBFLCh0gt9dxZYIx9W4rfJr7+hKe5jxxHmfOK8YSH4qsHLLN4Ck8JZ+v7Q5fIm1huErg==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.4.tgz", + "integrity": "sha512-yIxbspZb5kGCAHWm8dexALQ9en1IYDfErzjSEq1KzXFniHv019VT3mNtTK7t8qdy4TwT6QYHI9sEZabONHg+aw==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.4.tgz", + "integrity": "sha512-sywLRD3UK/qRJt0oBwdpYLBibk7KiRfbswmWRDabuncQYSlf8aLEEUor/oP6KRz8KEG+HoiVLBhPRD5JWjS8Sg==", "dev": true, "optional": true }, @@ -3135,9 +3134,9 @@ "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==" }, "@vscode/vsce": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.26.0.tgz", - "integrity": "sha512-v54ltgMzUG8lGY0kAgaOlry57xse1RlWzes9FotfGEx+Fr05KeR8rZicQzEMDmi9QnOgVWHuiEq+xA2HWkAz+Q==", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.26.1.tgz", + "integrity": "sha512-QOG6Ht7V93nhwcBxPWcG33UK0qDGEoJdg0xtVeaTN27W6PGdMJUJGTPhB/sNHUIFKwvwzv/zMAHvDgMNXbcwlA==", "dev": true, "requires": { "@azure/identity": "^4.1.0", @@ -3596,34 +3595,34 @@ "dev": true }, "esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.4.tgz", + "integrity": "sha512-sFMcNNrj+Q0ZDolrp5pDhH0nRPN9hLIM3fRPwgbLYJeSHHgnXSnbV3xYgSVuOeLWH9c73VwmEverVzupIv5xuA==", "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" + "@esbuild/aix-ppc64": "0.21.4", + "@esbuild/android-arm": "0.21.4", + "@esbuild/android-arm64": "0.21.4", + "@esbuild/android-x64": "0.21.4", + "@esbuild/darwin-arm64": "0.21.4", + "@esbuild/darwin-x64": "0.21.4", + "@esbuild/freebsd-arm64": "0.21.4", + "@esbuild/freebsd-x64": "0.21.4", + "@esbuild/linux-arm": "0.21.4", + "@esbuild/linux-arm64": "0.21.4", + "@esbuild/linux-ia32": "0.21.4", + "@esbuild/linux-loong64": "0.21.4", + "@esbuild/linux-mips64el": "0.21.4", + "@esbuild/linux-ppc64": "0.21.4", + "@esbuild/linux-riscv64": "0.21.4", + "@esbuild/linux-s390x": "0.21.4", + "@esbuild/linux-x64": "0.21.4", + "@esbuild/netbsd-x64": "0.21.4", + "@esbuild/openbsd-x64": "0.21.4", + "@esbuild/sunos-x64": "0.21.4", + "@esbuild/win32-arm64": "0.21.4", + "@esbuild/win32-ia32": "0.21.4", + "@esbuild/win32-x64": "0.21.4" } }, "escape-string-regexp": { @@ -4083,6 +4082,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { "yallist": "^4.0.0" } @@ -4449,12 +4449,9 @@ "dev": true }, "semver": { - "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" - } + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" }, "set-blocking": { "version": "2.0.0", @@ -4772,7 +4769,8 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "yauzl": { "version": "2.10.0", diff --git a/code/package.json b/code/package.json index 9d738186d..17c70e16f 100644 --- a/code/package.json +++ b/code/package.json @@ -31,15 +31,15 @@ ], "dependencies": { "@vscode/python-extension": "^1.0.5", - "semver": "^7.6.0", + "semver": "^7.6.2", "vscode-languageclient": "^9.0.1" }, "devDependencies": { "@types/glob": "^8.1.0", "@types/node": "^18", "@types/vscode": "1.78.0", - "@vscode/vsce": "^2.26.0", - "esbuild": "^0.20.2", + "@vscode/vsce": "^2.26.1", + "esbuild": "^0.21.4", "ovsx": "^0.9.1", "typescript": "^5.4.5" }, diff --git a/code/requirements.txt b/code/requirements.txt index fcd9a23a3..24d3a5fc5 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -2,21 +2,21 @@ # This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# pip-compile --generate-hashes ./requirements.in +# pip-compile --generate-hashes requirements.in # aiosqlite==0.20.0 \ --hash=sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6 \ --hash=sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7 # via -r requirements.in -attrs==23.1.0 \ - --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ - --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 +attrs==23.2.0 \ + --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ + --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 # via # cattrs # lsprotocol -cattrs==23.2.2 \ - --hash=sha256:66064e2060ea207c5a48d065ab1910c10bb8108c28f3df8d1a7b1aa6b19d191b \ - --hash=sha256:b790b1c2be1ce042611e33f740e343c2593918bbf3c1cc88cdddac4defc09655 +cattrs==23.2.3 \ + --hash=sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108 \ + --hash=sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f # via # lsprotocol # pygls @@ -24,17 +24,17 @@ docutils==0.20.1 \ --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \ --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b # via -r requirements.in -exceptiongroup==1.2.0 \ - --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \ - --hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68 +exceptiongroup==1.2.1 \ + --hash=sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad \ + --hash=sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16 # via cattrs lsprotocol==2023.0.1 \ --hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \ --hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d # via pygls -platformdirs==4.2.0 \ - --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ - --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 # via -r requirements.in pygls==1.3.1 \ --hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \ @@ -44,9 +44,9 @@ tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via -r requirements.in -typing-extensions==4.8.0 \ - --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ - --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef +typing-extensions==4.11.0 \ + --hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \ + --hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a # via # aiosqlite # cattrs diff --git a/code/tox.ini b/code/tox.ini deleted file mode 100644 index 0296ac9d1..000000000 --- a/code/tox.ini +++ /dev/null @@ -1,23 +0,0 @@ -[tox] -min_version = 4.0 - -[testenv:bundle-deps] -basepython = python3.8 -description = Install dependencies -skip_install = true -commands = - python --version - python -c "import sys; v = sys.version_info; sys.exit(v.major != 3 or v.minor != 8)" - python -m pip install -t ./bundled/libs --no-cache-dir --implementation py --no-deps --upgrade -r ./requirements.txt - python -m pip install -t ./bundled/libs --no-cache-dir --implementation py --no-deps --upgrade {env:ESBONIO_WHL} - -[testenv:update-deps] -basepython = python3.8 -description = Update bundled dependency versions -skip_install = true -deps = - pip-tools -commands = - python --version - python -c "import sys; v = sys.version_info; sys.exit(v.major != 3 or v.minor != 8)" - pip-compile --resolver=backtracking --generate-hashes --upgrade ./requirements.in diff --git a/docs/ext/domain.py b/docs/ext/domain.py index 6dda6bbf3..39b4d8ba9 100644 --- a/docs/ext/domain.py +++ b/docs/ext/domain.py @@ -1,4 +1,6 @@ -from typing import Dict +from __future__ import annotations + +import typing from docutils.parsers.rst import directives from sphinx import addnodes @@ -7,7 +9,19 @@ from sphinx.domains import Domain from sphinx.domains import ObjType from sphinx.roles import XRefRole -from sphinx.util.typing import OptionSpec +from sphinx.util.nodes import make_id +from sphinx.util.nodes import make_refnode + +if typing.TYPE_CHECKING: + from typing import Dict + from typing import Optional + from typing import Tuple + + from docutils.nodes import Element + from sphinx.addnodes import pending_xref + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec def config_scope(argument: str): @@ -45,6 +59,15 @@ def handle_signature(self, sig: str, signode: addnodes.desc_signature) -> str: signode += addnodes.desc_name(sig, sig) return sig + def add_target_and_index( + self, name: str, sig: str, signode: addnodes.desc_signature + ) -> None: + node_id = make_id(self.env, self.state.document, term=name) + signode["ids"].append(node_id) + + domain: EsbonioDomain = self.env.domains["esbonio"] + domain.config_values[name] = (self.env.docname, node_id) + class EsbonioDomain(Domain): """A domain dedicated to documenting the esbonio language server""" @@ -64,6 +87,33 @@ class EsbonioDomain(Domain): "conf": XRefRole(), } + initial_data = { + "config_values": {}, + } + + @property + def config_values(self) -> dict[str, Tuple[str, str]]: + return self.data.setdefault("config_values", {}) + + def resolve_xref( + self, + env: BuildEnvironment, + fromdocname: str, + builder: Builder, + type: str, + target: str, + node: pending_xref, + contnode: Element, + ) -> Optional[Element]: + """Resolve cross references""" + + if (entry := self.config_values.get(target, None)) is None: + return None + + return make_refnode( + builder, fromdocname, entry[0], entry[1], [contnode], target + ) + def setup(app: Sphinx): app.add_domain(EsbonioDomain) diff --git a/docs/lsp/howto/migrate-to-v1.rst b/docs/lsp/howto/migrate-to-v1.rst index 16fed68ba..fb9d0d9ba 100644 --- a/docs/lsp/howto/migrate-to-v1.rst +++ b/docs/lsp/howto/migrate-to-v1.rst @@ -15,13 +15,15 @@ This guide covers the breaking changes between the ``v0.x`` and ``v1.x`` version Installation Changes -------------------- -Previously, you would have had to install ``esbonio`` as a development dependency for every project you wished to use it in. -In ``v1.x`` this is no longer necessary, in fact, it's recommended you remove it from all of your project specific environments:: +Previously, it was recommended to install ``esbonio`` as a development dependency for each project you wished to use with. +This was because ``esbonio`` would run Sphinx as part of its own process and therefore need access to your project's dependencies. + +In ``v1.x`` Sphinx is now run in a separate process so this is no longer necessary, in fact, it's recommended you remove it from your project specific environments:: (env) $ pip uninstall esbonio -Instead, you should now have a single, global installation that can be reused across projects. -We recommend that you use `pipx `__ to manage this installation for you:: +Instead, you can now have a single, global installation that is reused across projects. +It's recommended that you use `pipx `__ to manage this installation for you:: $ pipx install esbonio # Installs esbonio globally in an isolated environment $ pipx upgrade esbonio # Upgrade esbonio and its dependencies @@ -37,7 +39,7 @@ Configuration Changes With the release of ``v1.x``, Esbonio's configuration system has been overhauled, see :ref:`lsp-configuration` for all of the available configuration options and methods. -While ``esbonio`` can now be installed globally, it still needs access to your project's development environment in order to properly understand it. +While ``esbonio`` can now be installed globally, it still needs access to your project's development environment in order launch Sphinx correctly. This means the two most imporant configuration values are - :esbonio:conf:`esbonio.sphinx.pythonCommand`: For telling ``esbonio`` the command it needs to run in order to use the correct Python environment. @@ -45,32 +47,44 @@ This means the two most imporant configuration values are The following table outlines the configuration options that have been removed in ``v1.x`` and what their correpsonding replacement is -+-----------------------------------------+-------------------------------------------------+-------------+ -| Removed Option | Replacement | Notes | -+=========================================+=================================================+=============+ -| - ``esbonio.server.hideSphinxOutput`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | | -| - ``esbonio.sphinx.buildDir`` | | | -| - ``esbonio.sphinx.builderName`` | | | -| - ``esbonio.sphinx.confDir`` | | | -| - ``esbonio.sphinx.doctreeDir`` | | | -| - ``esbonio.sphinx.forceFullBuild`` | | | -| - ``esbonio.sphinx.keepGoing`` | | | -| - ``esbonio.sphinx.makeMode`` | | | -| - ``esbonio.sphinx.numJobs`` | | | -| - ``esbonio.sphinx.quiet`` | | | -| - ``esbonio.sphinx.silent`` | | | -| - ``esbonio.sphinx.srcDir`` | | | -| - ``esbonio.sphinx.tags`` | | | -| - ``esbonio.sphinx.verbosity`` | | | -| - ``esbonio.sphinx.warningIsError`` | | | -+-----------------------------------------+-------------------------------------------------+-------------+ -| ``esbonio.server.logLevel`` | :esbonio:conf:`esbonio.logging.level` | | -+-----------------------------------------+-------------------------------------------------+-------------+ -| ``esbonio.server.logFilter`` | :esbonio:conf:`esbonio.logging.config` | | -+-----------------------------------------+-------------------------------------------------+-------------+ -| ``esbonio.server.enabledInPyFiles`` | :esbonio:conf:`esbonio.server.documentSelector` | VSCode only | -+-----------------------------------------+-------------------------------------------------+-------------+ -| - ``esbonio.server.installBehavior`` | N/A | VSCode only,| -| - ``esbonio.server.updateBehavior`` | | no longer | -| - ``esbonio.server.updateFrequency`` | | required. | -+-----------------------------------------+-------------------------------------------------+-------------+ ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| Removed Option | Replacement | Notes | ++=========================================+=================================================+==============================================================+ +| - ``esbonio.sphinx.builderName`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Pass ``-b `` to | +| - ``esbonio.sphinx.srcDir`` | | ``sphinx-build`` | +| - ``esbonio.sphinx.buildDir`` | | | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.confDir`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``-c `` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.doctreeDir`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``-d `` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.forceFullBuild`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``-E`` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.keepGoing`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``--keep-going`` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.makeMode`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Pass ``-M `` to | +| | | ``sphinx-build`` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.numJobs`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``-j `` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.quiet`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``-q`` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.tags`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``-t`` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.verbosity`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``-v`` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.sphinx.warningIsError`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``-W`` | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| - ``esbonio.server.hideSphinxOutput`` | :esbonio:conf:`esbonio.sphinx.buildCommand` | Use ``-Q`` | +| - ``esbonio.sphinx.silent`` | | | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.server.logLevel`` | :esbonio:conf:`esbonio.logging.level` | | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.server.logFilter`` | :esbonio:conf:`esbonio.logging.config` | | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| ``esbonio.server.enabledInPyFiles`` | :esbonio:conf:`esbonio.server.documentSelector` | VSCode only | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ +| - ``esbonio.server.installBehavior`` | N/A | VSCode only, no longer required. | +| - ``esbonio.server.updateBehavior`` | | | +| - ``esbonio.server.updateFrequency`` | | | ++-----------------------------------------+-------------------------------------------------+--------------------------------------------------------------+ diff --git a/docs/lsp/reference/configuration.rst b/docs/lsp/reference/configuration.rst index d377e60c6..e58cf7b8d 100644 --- a/docs/lsp/reference/configuration.rst +++ b/docs/lsp/reference/configuration.rst @@ -286,17 +286,6 @@ Preview The following options control the behavior of the preview -.. esbonio:config:: esbonio.sphinx.enableSyncScrolling - :scope: project - :type: boolean - - Enable support for syncronsied scrolling between the editor and preview pane - - .. note:: - - In order to use syncronised scrolling, dedicated support for it needs to be implemented by your language client. - See :ref:`lsp-feat-sync-scrolling` for details. - .. esbonio:config:: esbonio.preview.bind :scope: project :type: string diff --git a/lib/esbonio/.gitignore b/lib/esbonio/.gitignore index b2be92b7d..3b0d2daec 100644 --- a/lib/esbonio/.gitignore +++ b/lib/esbonio/.gitignore @@ -1 +1,2 @@ +.coverage* result diff --git a/lib/esbonio/Makefile b/lib/esbonio/Makefile index 03bcea6f2..5573879f5 100644 --- a/lib/esbonio/Makefile +++ b/lib/esbonio/Makefile @@ -1,9 +1,17 @@ -PY ?= 310 +include ../../.devcontainer/tools.mk -.PHONY: develop test -develop: - nix develop .#py$(PY) +# Global flags to pass to hatch, e.g. -v, --no-color etc. +HATCH_ARGS = -test: - nix develop .#py$(PY) --command pytest +.PHONY: dist release test + +dist: $(HATCH) + $(HATCH) $(HATCH_ARGS) build + +test: ARGS ?= -i py=$(shell $(PY) -c 'import sys;v=sys.version_info;print(f"{v.major}.{v.minor}")') +test: $(HATCH) $(PY) + $(HATCH) $(HATCH_ARGS) test $(ARGS) + +release: $(TOWNCRIER) $(HATCH) $(PY) + $(PY) ../scripts/make_release.py lsp diff --git a/lib/esbonio/changes/413.api.md b/lib/esbonio/changes/413.api.md new file mode 100644 index 000000000..a8fa3c4fd --- /dev/null +++ b/lib/esbonio/changes/413.api.md @@ -0,0 +1 @@ +In the server's context `LanguageFeatures` can now define a `CompletionTrigger` which among other things, allows them to declare trigger characters diff --git a/lib/esbonio/changes/782.fix.md b/lib/esbonio/changes/782.fix.md new file mode 100644 index 000000000..0941c4744 --- /dev/null +++ b/lib/esbonio/changes/782.fix.md @@ -0,0 +1 @@ +The language server should now launch the correct version of the sphinx agent, even if an installation of `esbonio` exists in the target environment diff --git a/lib/esbonio/changes/799.enhancement.md b/lib/esbonio/changes/799.enhancement.md new file mode 100644 index 000000000..9f7655f9c --- /dev/null +++ b/lib/esbonio/changes/799.enhancement.md @@ -0,0 +1 @@ +The server now includes the `eval-rst` directive in its completion suggestions for MyST files diff --git a/lib/esbonio/changes/800.fix.md b/lib/esbonio/changes/800.fix.md new file mode 100644 index 000000000..ea0a0973b --- /dev/null +++ b/lib/esbonio/changes/800.fix.md @@ -0,0 +1 @@ +The server no longer raises `ValueErrors` when typing `:` characters in markdown files. diff --git a/lib/esbonio/changes/810.fix.md b/lib/esbonio/changes/810.fix.md new file mode 100644 index 000000000..f4a5713bd --- /dev/null +++ b/lib/esbonio/changes/810.fix.md @@ -0,0 +1 @@ +The server should no longer throw path mount errors when used across partitions on Windows diff --git a/lib/esbonio/changes/823.api.md b/lib/esbonio/changes/823.api.md new file mode 100644 index 000000000..f14bf56a9 --- /dev/null +++ b/lib/esbonio/changes/823.api.md @@ -0,0 +1,2 @@ +In the Sphinx process, it is now possible for extensions to declare a role target provider through the `app.esbonio.create_role_target_provider` and `app.esbonio.add_role` methods. +**Note:** This does require an associated implementation of the target provider on the server side. diff --git a/lib/esbonio/changes/823.feature.md b/lib/esbonio/changes/823.feature.md new file mode 100644 index 000000000..28410fcd0 --- /dev/null +++ b/lib/esbonio/changes/823.feature.md @@ -0,0 +1 @@ +Implement role target completions for MyST syntax diff --git a/lib/esbonio/esbonio/server/__init__.py b/lib/esbonio/esbonio/server/__init__.py index c06bbc762..4b7f4b238 100644 --- a/lib/esbonio/esbonio/server/__init__.py +++ b/lib/esbonio/esbonio/server/__init__.py @@ -4,6 +4,7 @@ from .events import EventSource from .feature import CompletionConfig from .feature import CompletionContext +from .feature import CompletionTrigger from .feature import LanguageFeature from .server import EsbonioLanguageServer from .server import EsbonioWorkspace @@ -15,6 +16,7 @@ "ConfigChangeEvent", "CompletionConfig", "CompletionContext", + "CompletionTrigger", "EsbonioLanguageServer", "EsbonioWorkspace", "EventSource", diff --git a/lib/esbonio/esbonio/server/cli.py b/lib/esbonio/esbonio/server/cli.py index 2de49a408..343a501fb 100644 --- a/lib/esbonio/esbonio/server/cli.py +++ b/lib/esbonio/esbonio/server/cli.py @@ -70,6 +70,10 @@ def main(argv: Optional[Sequence[str]] = None): "esbonio.server.features.preview_manager", "esbonio.server.features.directives", "esbonio.server.features.roles", + "esbonio.server.features.rst.directives", + "esbonio.server.features.rst.roles", + "esbonio.server.features.myst.directives", + "esbonio.server.features.myst.roles", "esbonio.server.features.sphinx_support.diagnostics", "esbonio.server.features.sphinx_support.symbols", "esbonio.server.features.sphinx_support.directives", diff --git a/lib/esbonio/esbonio/server/events.py b/lib/esbonio/esbonio/server/events.py index d44e16334..255ee6299 100644 --- a/lib/esbonio/esbonio/server/events.py +++ b/lib/esbonio/esbonio/server/events.py @@ -21,7 +21,6 @@ class EventSource: # 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.""" diff --git a/lib/esbonio/esbonio/server/feature.py b/lib/esbonio/esbonio/server/feature.py index e34712a5d..787610201 100644 --- a/lib/esbonio/esbonio/server/feature.py +++ b/lib/esbonio/esbonio/server/feature.py @@ -16,6 +16,7 @@ from typing import Coroutine from typing import List from typing import Optional + from typing import Set from typing import Union from .server import EsbonioLanguageServer @@ -81,7 +82,7 @@ def document_open(self, params: types.DidOpenTextDocumentParams) -> MaybeAsyncNo def document_save(self, params: types.DidSaveTextDocumentParams) -> MaybeAsyncNone: """Called when a text document is saved.""" - completion_triggers: List["re.Pattern"] = [] + completion_trigger: Optional[CompletionTrigger] = None def completion(self, context: CompletionContext) -> CompletionResult: """Called when a completion request matches one of the specified triggers.""" @@ -90,13 +91,110 @@ def document_symbol( self, params: types.DocumentSymbolParams ) -> DocumentSymbolResult: """Called when a document symbols request is received.""" - ... def workspace_symbol( self, params: types.WorkspaceSymbolParams ) -> WorkspaceSymbolResult: """Called when a workspace symbols request is received.""" - ... + + +@attrs.define +class CompletionTrigger: + """Define when the feature's completion method should be called.""" + + patterns: List[re.Pattern] + """A list of regular expressions to try""" + + languages: Set[str] = attrs.field(factory=set) + """Languages in which the completion trigger should fire. + + If empty, the document's language will be ignored. + """ + + characters: Set[str] = attrs.field(factory=set) + """Characters which, when typed, should trigger a completion request. + + If empty, this trigger will ignore any trigger characters. + """ + + def __call__( + self, + uri: Uri, + params: types.CompletionParams, + document: TextDocument, + language: str, + client_capabilities: types.ClientCapabilities, + ) -> Optional[CompletionContext]: + """Determine if this completion trigger should fire. + + Parameters + ---------- + uri + The uri of the document in which the completion request was made + + params + The completion params sent from the client + + document + The document in which the completion request was made + + language + The language at the point where the completion request was made + + client_capabilities + The client's capabilities + + Returns + ------- + Optional[CompletionContext] + A completion context, if this trigger has fired + """ + + if len(self.languages) > 0 and language not in self.languages: + return None + + if not self._trigger_characters_match(params): + return None + + try: + line = document.lines[params.position.line] + except IndexError: + line = "" + + for pattern in self.patterns: + for match in pattern.finditer(line): + # Only trigger completions if the position of the request is within the + # match. + start, stop = match.span() + if not (start <= params.position.character <= stop): + continue + + return CompletionContext( + uri=uri, + doc=document, + match=match, + position=params.position, + language=language, + capabilities=client_capabilities, + ) + + return None + + def _trigger_characters_match(self, params: types.CompletionParams) -> bool: + """Determine if this trigger's completion characters align with the request.""" + + if (context := params.context) is None: + # No context available, assume a match + return True + + if context.trigger_kind != types.CompletionTriggerKind.TriggerCharacter: + # Not a trigger character request, assume a match + return True + + if (char := context.trigger_character) is None or len(self.characters) == 0: + return True + + return char in self.characters @attrs.define @@ -120,12 +218,15 @@ class CompletionContext: doc: TextDocument """The document within which the completion request was made.""" - match: "re.Match" + match: re.Match """The match object describing the site of the completion request.""" position: types.Position """The position at which the completion request was made.""" + language: str + """The language where the completion request was made.""" + capabilities: types.ClientCapabilities """The client's capabilities.""" diff --git a/lib/esbonio/esbonio/server/features/directives/__init__.py b/lib/esbonio/esbonio/server/features/directives/__init__.py index fdbc4d461..5d03bc22e 100644 --- a/lib/esbonio/esbonio/server/features/directives/__init__.py +++ b/lib/esbonio/esbonio/server/features/directives/__init__.py @@ -4,13 +4,8 @@ import typing import attrs -from lsprotocol import types from esbonio import server -from esbonio.sphinx_agent.types import MYST_DIRECTIVE -from esbonio.sphinx_agent.types import RST_DIRECTIVE - -from . import completion if typing.TYPE_CHECKING: from typing import Any @@ -45,13 +40,16 @@ def suggest_directives( class DirectiveFeature(server.LanguageFeature): - """Support for directives.""" + """'Backend' support for directives. + + It's this language feature's responsibility to provide an API that exposes the + information a "frontend" language feature may want. + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._providers: Dict[int, DirectiveProvider] = {} - self._insert_behavior = "replace" def add_provider(self, provider: DirectiveProvider): """Register a directive provider. @@ -63,72 +61,6 @@ def add_provider(self, provider: DirectiveProvider): """ self._providers[id(provider)] = provider - completion_triggers = [RST_DIRECTIVE, MYST_DIRECTIVE] - - def initialized(self, params: types.InitializedParams): - """Called once the initial handshake between client and server has finished.""" - self.configuration.subscribe( - "esbonio.server.completion", - server.CompletionConfig, - self.update_configuration, - ) - - def update_configuration( - self, event: server.ConfigChangeEvent[server.CompletionConfig] - ): - """Called when the user's configuration is updated.""" - self._insert_behavior = event.value.preferred_insert_behavior - - async def completion( - self, context: server.CompletionContext - ) -> Optional[List[types.CompletionItem]]: - """Provide completion suggestions for directives.""" - - groups = context.match.groupdict() - - # Are we completing a directive's options? - if "directive" not in groups: - return await self.complete_options(context) - - # Don't offer completions for targets - if (groups["name"] or "").startswith("_"): - return None - - # 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 await self.complete_arguments(context) - - return await self.complete_directives(context) - - async def complete_options(self, context: server.CompletionContext): - return None - - async def complete_arguments(self, context: server.CompletionContext): - return None - - async def complete_directives( - self, context: server.CompletionContext - ) -> Optional[List[types.CompletionItem]]: - """Return completion suggestions for the available directives.""" - - language = self.server.get_language_at(context.doc, context.position) - render_func = completion.get_directive_renderer(language, self._insert_behavior) - if render_func is None: - return None - - items = [] - for directive in await self.suggest_directives(context): - if (item := render_func(context, directive)) is not None: - items.append(item) - - if len(items) > 0: - return items - - return None - async def suggest_directives( self, context: server.CompletionContext ) -> List[Directive]: diff --git a/lib/esbonio/esbonio/server/features/myst/__init__.py b/lib/esbonio/esbonio/server/features/myst/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/esbonio/esbonio/server/features/myst/directives.py b/lib/esbonio/esbonio/server/features/myst/directives.py new file mode 100644 index 000000000..aec577aca --- /dev/null +++ b/lib/esbonio/esbonio/server/features/myst/directives.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import typing + +from lsprotocol import types + +from esbonio import server +from esbonio.server.features.directives import Directive +from esbonio.server.features.directives import DirectiveFeature +from esbonio.server.features.directives import completion +from esbonio.sphinx_agent.types import MYST_DIRECTIVE + +if typing.TYPE_CHECKING: + from typing import List + from typing import Optional + + +class MystDirectives(server.LanguageFeature): + """A frontend to directives for MyST syntax.""" + + def __init__(self, directives: DirectiveFeature, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.directives = directives + self._insert_behavior = "replace" + + completion_trigger = server.CompletionTrigger( + patterns=[MYST_DIRECTIVE], + languages={"markdown"}, + characters={".", "`", "/"}, + ) + + def initialized(self, params: types.InitializedParams): + """Called once the initial handshake between client and server has finished.""" + self.configuration.subscribe( + "esbonio.server.completion", + server.CompletionConfig, + self.update_configuration, + ) + + def update_configuration( + self, event: server.ConfigChangeEvent[server.CompletionConfig] + ): + """Called when the user's configuration is updated.""" + self._insert_behavior = event.value.preferred_insert_behavior + + async def completion( + self, context: server.CompletionContext + ) -> Optional[List[types.CompletionItem]]: + """Provide completion suggestions for directives.""" + + groups = context.match.groupdict() + + # Are we completing a directive's options? + if "directive" not in groups: + return await self.complete_options(context) + + # Don't offer completions for targets + if (groups["name"] or "").startswith("_"): + return None + + # 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 await self.complete_arguments(context) + + return await self.complete_directives(context) + + async def complete_options(self, context: server.CompletionContext): + return None + + async def complete_arguments(self, context: server.CompletionContext): + return None + + async def complete_directives( + self, context: server.CompletionContext + ) -> Optional[List[types.CompletionItem]]: + """Return completion suggestions for the available directives.""" + + render_func = completion.get_directive_renderer( + context.language, self._insert_behavior + ) + if render_func is None: + return None + + items = [] + + # Include the special `eval-rst` directive + eval_rst = Directive("eval-rst", implementation=None) + if (item := render_func(context, eval_rst)) is not None: + items.append(item) + + for directive in await self.directives.suggest_directives(context): + if (item := render_func(context, directive)) is not None: + items.append(item) + + if len(items) > 0: + return items + + return None + + +def esbonio_setup(esbonio: server.EsbonioLanguageServer, directives: DirectiveFeature): + myst_directives = MystDirectives(directives, esbonio) + esbonio.add_feature(myst_directives) diff --git a/lib/esbonio/esbonio/server/features/myst/roles.py b/lib/esbonio/esbonio/server/features/myst/roles.py new file mode 100644 index 000000000..dbc01bda0 --- /dev/null +++ b/lib/esbonio/esbonio/server/features/myst/roles.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import typing + +from lsprotocol import types + +from esbonio import server +from esbonio.server.features.roles import RolesFeature +from esbonio.server.features.roles import completion +from esbonio.sphinx_agent.types import MYST_ROLE + +if typing.TYPE_CHECKING: + from typing import List + from typing import Optional + + +class MystRoles(server.LanguageFeature): + """A frontend to roles for MyST syntax.""" + + def __init__(self, roles: RolesFeature, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.roles = roles + self._insert_behavior = "replace" + + completion_trigger = server.CompletionTrigger( + patterns=[MYST_ROLE], + languages={"markdown"}, + characters={"{", "`", "<", "/"}, + ) + + def initialized(self, params: types.InitializedParams): + """Called once the initial handshake between client and server has finished.""" + self.configuration.subscribe( + "esbonio.server.completion", + server.CompletionConfig, + self.update_configuration, + ) + + def update_configuration( + self, event: server.ConfigChangeEvent[server.CompletionConfig] + ): + """Called when the user's configuration is updated.""" + self._insert_behavior = event.value.preferred_insert_behavior + + async def completion( + self, context: server.CompletionContext + ) -> Optional[List[types.CompletionItem]]: + """Provide completion suggestions for roles.""" + + 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 await self.complete_targets(context) + + return await self.complete_roles(context) + + async def complete_targets(self, context: server.CompletionContext): + """Provide completion suggestions for role targets.""" + + render_func = completion.get_role_target_renderer( + context.language, self._insert_behavior + ) + if render_func is None: + return None + + items = [] + role_name = context.match.group("name") + for target in await self.roles.suggest_targets(context, role_name): + if (item := render_func(context, target)) is not None: + items.append(item) + + return items if len(items) > 0 else None + + async def complete_roles( + self, context: server.CompletionContext + ) -> Optional[List[types.CompletionItem]]: + """Return completion suggestions for the available roles""" + + render_func = completion.get_role_renderer( + context.language, self._insert_behavior + ) + if render_func is None: + return None + + items = [] + for role in await self.roles.suggest_roles(context): + if (item := render_func(context, role)) is not None: + items.append(item) + + if len(items) > 0: + return items + + return None + + +def esbonio_setup(esbonio: server.EsbonioLanguageServer, roles: RolesFeature): + rst_roles = MystRoles(roles, esbonio) + esbonio.add_feature(rst_roles) diff --git a/lib/esbonio/esbonio/server/features/preview_manager/__init__.py b/lib/esbonio/esbonio/server/features/preview_manager/__init__.py index 8b44002ac..5436926f9 100644 --- a/lib/esbonio/esbonio/server/features/preview_manager/__init__.py +++ b/lib/esbonio/esbonio/server/features/preview_manager/__init__.py @@ -146,7 +146,6 @@ async def scroll_view(self, line: int): self.webview.scroll(line) async def preview_file(self, params, retry=True): - if self.preview is None: return None @@ -161,15 +160,12 @@ async def preview_file(self, params, retry=True): return None if (build_path := await project.get_build_path(src_uri)) is None: - # The client might not have built the project yet. if client.id not in self.built_clients and retry is True: - # Only retry this once. await self.sphinx.trigger_build(src_uri) return await self.preview_file(params, retry=False) else: - self.logger.debug( "Unable to preview file '%s', not included in build output.", src_uri, diff --git a/lib/esbonio/esbonio/server/features/preview_manager/preview.py b/lib/esbonio/esbonio/server/features/preview_manager/preview.py index a69d2aa46..1bae234d1 100644 --- a/lib/esbonio/esbonio/server/features/preview_manager/preview.py +++ b/lib/esbonio/esbonio/server/features/preview_manager/preview.py @@ -58,7 +58,6 @@ class PreviewServer: """The http server that serves the built content.""" def __init__(self, logger: logging.Logger, config: PreviewConfig, executor: Any): - self.config = config """The current configuration.""" diff --git a/lib/esbonio/esbonio/server/features/project_manager/manager.py b/lib/esbonio/esbonio/server/features/project_manager/manager.py index 562eddb7f..052365b71 100644 --- a/lib/esbonio/esbonio/server/features/project_manager/manager.py +++ b/lib/esbonio/esbonio/server/features/project_manager/manager.py @@ -26,7 +26,7 @@ def __init__(self, *args, **kwargs): 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) + self.projects[scope] = Project(dbpath, self.converter) def get_project(self, uri: Uri) -> Optional[Project]: """Return the project instance for the given uri, if available""" diff --git a/lib/esbonio/esbonio/server/features/project_manager/project.py b/lib/esbonio/esbonio/server/features/project_manager/project.py index 8e15d4d64..e06de6226 100644 --- a/lib/esbonio/esbonio/server/features/project_manager/project.py +++ b/lib/esbonio/esbonio/server/features/project_manager/project.py @@ -15,13 +15,20 @@ from typing import List from typing import Optional from typing import Tuple + from typing import Type + from typing import TypeVar from typing import Union + import cattrs + + T = TypeVar("T") + class Project: """Represents a documentation project.""" - def __init__(self, dbpath: Union[str, pathlib.Path]): + def __init__(self, dbpath: Union[str, pathlib.Path], converter: cattrs.Converter): + self.converter = converter self.dbpath = dbpath self._connection: Optional[aiosqlite.Connection] = None @@ -35,6 +42,9 @@ async def get_db(self) -> aiosqlite.Connection: return self._connection + def load_as(self, o: str, t: Type[T]) -> T: + return self.converter.structure(json.loads(o), t) + async def get_src_uris(self) -> List[Uri]: """Return all known source uris.""" db = await self.get_db() @@ -76,6 +86,16 @@ async def get_directives(self) -> List[Tuple[str, Optional[str]]]: cursor = await db.execute(query) return await cursor.fetchall() # type: ignore[return-value] + async def get_role(self, name: str) -> Optional[types.Role]: + """Get the roles known to Sphinx.""" + db = await self.get_db() + + query = "SELECT * FROM roles WHERE name = ?" + cursor = await db.execute(query, (name,)) + result = await cursor.fetchone() + + return types.Role.from_db(self.load_as, *result) if result is not None else None + async def get_roles(self) -> List[Tuple[str, Optional[str]]]: """Get the roles known to Sphinx.""" db = await self.get_db() diff --git a/lib/esbonio/esbonio/server/features/roles/__init__.py b/lib/esbonio/esbonio/server/features/roles/__init__.py index 75b68a092..ad19eb579 100644 --- a/lib/esbonio/esbonio/server/features/roles/__init__.py +++ b/lib/esbonio/esbonio/server/features/roles/__init__.py @@ -3,15 +3,10 @@ import inspect import typing -import attrs -from lsprotocol import types +from lsprotocol import types as lsp from esbonio import server -from esbonio.sphinx_agent.types import MYST_ROLE -from esbonio.sphinx_agent.types import RST_DIRECTIVE -from esbonio.sphinx_agent.types import RST_ROLE - -from . import completion +from esbonio.sphinx_agent import types if typing.TYPE_CHECKING: from typing import Any @@ -21,38 +16,63 @@ from typing import Optional from typing import Union + from esbonio.server import Uri -@attrs.define -class Role: - """Represents a role.""" - name: str - """The name of the role, as the user would type in an rst file.""" +class RoleProvider: + """Base class for role providers.""" - implementation: Optional[str] - """The dotted name of the role's implementation.""" + def get_role( + self, uri: Uri, name: str + ) -> Union[Optional[types.Role], Coroutine[Any, Any, Optional[types.Role]]]: + """Return the definition of the given role, if known. + Parameters + ---------- + uri + The uri of the document in which the role name appears -class RoleProvider: - """Base class for role providers.""" + name + The name of the role, as the user would type in a document + """ + return None def suggest_roles( self, context: server.CompletionContext - ) -> Union[Optional[List[Role]], Coroutine[Any, Any, Optional[List[Role]]]]: + ) -> Union[ + Optional[List[types.Role]], Coroutine[Any, Any, Optional[List[types.Role]]] + ]: """Givem a completion context, suggest roles that may be used.""" return None +class RoleTargetProvider: + """Base class for role target providers.""" + + def suggest_targets( + self, context: server.CompletionContext, **kwargs + ) -> Union[ + Optional[List[lsp.CompletionItem]], + Coroutine[Any, Any, Optional[List[lsp.CompletionItem]]], + ]: + """Givem a completion context, suggest role targets that may be used.""" + return None + + class RolesFeature(server.LanguageFeature): - """Support for roles.""" + """Backend support for roles. + + It's this language feature's responsibility to provide an API that exposes the + information a frontend feature may want. + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._providers: Dict[int, RoleProvider] = {} - self._insert_behavior = "replace" + self._role_providers: Dict[int, RoleProvider] = {} + self._target_providers: Dict[str, RoleTargetProvider] = {} - def add_provider(self, provider: RoleProvider): + def add_role_provider(self, provider: RoleProvider): """Register a role provider. Parameters @@ -60,119 +80,126 @@ def add_provider(self, provider: RoleProvider): provider The role provider """ - self._providers[id(provider)] = provider - - def initialized(self, params: types.InitializedParams): - """Called once the initial handshake between client and server has finished.""" - self.configuration.subscribe( - "esbonio.server.completion", - server.CompletionConfig, - self.update_configuration, - ) + self._role_providers[id(provider)] = provider + + def add_target_provider(self, name: str, provider: RoleTargetProvider): + """Register a role target provider. - def update_configuration( - self, event: server.ConfigChangeEvent[server.CompletionConfig] - ): - """Called when the user's configuration is updated.""" - self._insert_behavior = event.value.preferred_insert_behavior + Parameters + ---------- + provider + The role target provider + """ + if (existing := self._target_providers.get(name)) is not None: + raise ValueError( + f"RoleTargetProvider {provider!r} conflicts with existing " + f"provider: {existing!r}" + ) - completion_triggers = [MYST_ROLE, RST_ROLE] + self._target_providers[name] = provider - async def completion( + async def suggest_roles( self, context: server.CompletionContext - ) -> Optional[List[types.CompletionItem]]: - """Provide completion suggestions for roles.""" - - language = self.server.get_language_at(context.doc, context.position) - 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 await self.complete_targets(context) - - # If there's no indent, or this is a markdown document, then this can only be a - # role definition - indent = context.match.group(1) - if indent == "" or language == "markdown": - return await 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 await 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 RST_DIRECTIVE.match(line): - return [] + ) -> List[types.Role]: + """Suggest roles that may be used, given a completion context. - return await self.complete_roles(context) + Parameters + ---------- + context + The completion context + """ + items: List[types.Role] = [] - async def complete_targets(self, context: server.CompletionContext): - return None + for provider in self._role_providers.values(): + try: + result: Optional[List[types.Role]] = None - async def complete_roles( - self, context: server.CompletionContext - ) -> Optional[List[types.CompletionItem]]: - """Return completion suggestions for the available roles""" + aresult = provider.suggest_roles(context) + if inspect.isawaitable(aresult): + result = await aresult + + if result: + items.extend(result) + except Exception: + name = type(provider).__name__ + self.logger.error("Error in '%s.suggest_roles'", name, exc_info=True) + + return items + + async def get_role(self, uri: Uri, name: str) -> Optional[types.Role]: + """Return the definition of the given role name. + + Parameters + ---------- + uri + The uri of the document in which the role name appears + + name + The name of the role, as the user would type into a document. - language = self.server.get_language_at(context.doc, context.position) - render_func = completion.get_role_renderer(language, self._insert_behavior) - if render_func is None: - return None + Returns + ------- + Optional[types.Role] + The role's definition, if known + """ + for provider in self._role_providers.values(): + try: + result: Optional[types.Role] = None - items = [] - for role in await self.suggest_roles(context): - if (item := render_func(context, role)) is not None: - items.append(item) + aresult = provider.get_role(uri, name) + if inspect.isawaitable(aresult): + result = await aresult - if len(items) > 0: - return items + if result is not None: + return result + except Exception: + name = type(provider).__name__ + self.logger.error("Error in '%s.get_role'", name, exc_info=True) return None - async def suggest_roles(self, context: server.CompletionContext) -> List[Role]: - """Suggest roles that may be used, given a completion context. + async def suggest_targets( + self, context: server.CompletionContext, role_name: str + ) -> List[lsp.CompletionItem]: + """Suggest role targets that may be used, given a completion context. Parameters ---------- context The completion context + + role_name + The role to suggest targets for """ - items: List[Role] = [] + if (role := await self.get_role(context.uri, role_name)) is None: + self.logger.debug("Unknown role '%s'", role_name) + return [] + + targets = [] + self.logger.debug( + "Suggesting targets for role: '%s' (%s)", role.name, role.implementation + ) + + for spec in role.target_providers: + if (provider := self._target_providers.get(spec.name)) is None: + self.logger.error("Unknown target provider: '%s'", spec.name) + continue - for provider in self._providers.values(): try: - result: Optional[List[Role]] = None + result: Optional[List[lsp.CompletionItem]] = None - aresult = provider.suggest_roles(context) + aresult = provider.suggest_targets(context, **spec.kwargs) if inspect.isawaitable(aresult): result = await aresult - if result: - items.extend(result) + if result is not None: + targets.extend(result) + except Exception: name = type(provider).__name__ - self.logger.error("Error in '%s.suggest_roles'", name, exc_info=True) + self.logger.error("Error in '%s.suggest_targets'", name, exc_info=True) - return items + return targets def esbonio_setup(server: server.EsbonioLanguageServer): diff --git a/lib/esbonio/esbonio/server/features/roles/completion.py b/lib/esbonio/esbonio/server/features/roles/completion.py index 1d4a8818e..6c5db338e 100644 --- a/lib/esbonio/esbonio/server/features/roles/completion.py +++ b/lib/esbonio/esbonio/server/features/roles/completion.py @@ -15,20 +15,27 @@ from typing import Optional from typing import Tuple - from . import Role + from esbonio.sphinx_agent.types import Role RoleRenderer = Callable[ [server.CompletionContext, Role], Optional[types.CompletionItem] ] + RoleTargetRenderer = Callable[ + [server.CompletionContext, types.CompletionItem], Optional[types.CompletionItem] + ] + WORD = re.compile("[a-zA-Z]+") _ROLE_RENDERERS: Dict[Tuple[str, str], RoleRenderer] = {} """CompletionItem rendering functions for roles.""" +_ROLE_TARGET_RENDERERS: Dict[Tuple[str, str], RoleTargetRenderer] = {} +"""CompletionItem rendering functions for role targets.""" -def renderer(*, language: str, insert_behavior: str): - """Define a new rendering function.""" + +def role_renderer(*, language: str, insert_behavior: str): + """Define a new rendering function for roles.""" def fn(f: RoleRenderer) -> RoleRenderer: _ROLE_RENDERERS[(language, insert_behavior)] = f @@ -37,6 +44,16 @@ def fn(f: RoleRenderer) -> RoleRenderer: return fn +def role_target_renderer(*, language: str, insert_behavior: str): + """Define a new rendering function for role targets.""" + + def fn(f: RoleTargetRenderer) -> RoleTargetRenderer: + _ROLE_TARGET_RENDERERS[(language, insert_behavior)] = f + return f + + return fn + + def get_role_renderer(language: str, insert_behavior: str) -> Optional[RoleRenderer]: """Return the role renderer to use. @@ -56,7 +73,28 @@ def get_role_renderer(language: str, insert_behavior: str) -> Optional[RoleRende return _ROLE_RENDERERS.get((language, insert_behavior), None) -@renderer(language="rst", insert_behavior="insert") +def get_role_target_renderer( + language: str, insert_behavior: str +) -> Optional[RoleTargetRenderer]: + """Return the role target renderer to use. + + Parameters + ---------- + language + The source language the completion item will be inserted into + + insert_behavior + How the completion should behave when inserted. + + Returns + ------- + Optional[RoleTargetRenderer] + The rendering function to use that matches the given criteria, if available. + """ + return _ROLE_TARGET_RENDERERS.get((language, insert_behavior), None) + + +@role_renderer(language="rst", insert_behavior="insert") def render_rst_role_with_insert_text( context: server.CompletionContext, role: Role ) -> Optional[types.CompletionItem]: @@ -104,7 +142,31 @@ def render_rst_role_with_insert_text( return item -@renderer(language="markdown", insert_behavior="insert") +@role_target_renderer(language="rst", insert_behavior="replace") +def render_rst_target_with_text_edit( + context: server.CompletionContext, item: types.CompletionItem +) -> Optional[types.CompletionItem]: + """Render a ``CompletionItem`` using ``insertText``. + + This implements the ``replace`` insert behavior for role targets. + + Parameters + ---------- + context + The context in which the completion is being generated. + + item + The ``CompletionItem`` representing the role target. + + Returns + ------- + Optional[types.CompletionItem] + The rendered completion item, or ``None`` if the item should be skipped + """ + return item + + +@role_renderer(language="markdown", insert_behavior="insert") def render_myst_role_with_insert_text( context: server.CompletionContext, role: Role ) -> Optional[types.CompletionItem]: @@ -152,7 +214,7 @@ def render_myst_role_with_insert_text( return item -@renderer(language="rst", insert_behavior="replace") +@role_renderer(language="rst", insert_behavior="replace") def render_rst_role_with_text_edit( context: server.CompletionContext, role: Role ) -> Optional[types.CompletionItem]: @@ -195,7 +257,7 @@ def render_rst_role_with_text_edit( return item -@renderer(language="markdown", insert_behavior="replace") +@role_renderer(language="markdown", insert_behavior="replace") def render_myst_role_with_text_edit( context: server.CompletionContext, role: Role ) -> Optional[types.CompletionItem]: @@ -238,6 +300,30 @@ def render_myst_role_with_text_edit( return item +@role_target_renderer(language="markdown", insert_behavior="replace") +def render_myst_target_with_text_edit( + context: server.CompletionContext, item: types.CompletionItem +) -> Optional[types.CompletionItem]: + """Render a ``CompletionItem`` using ``textEdit``. + + This implements the ``replace`` insert behavior for role targets. + + Parameters + ---------- + context + The context in which the completion is being generated. + + item + The ``CompletionItem`` representing the role target. + + Returns + ------- + Optional[types.CompletionItem] + The rendered completion item, or ``None`` if the item should be skipped + """ + return item + + def _render_role_common(role: Role) -> types.CompletionItem: """Render the common fields of a role's completion item.""" return types.CompletionItem( diff --git a/lib/esbonio/esbonio/server/features/rst/__init__.py b/lib/esbonio/esbonio/server/features/rst/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/esbonio/esbonio/server/features/rst/directives.py b/lib/esbonio/esbonio/server/features/rst/directives.py new file mode 100644 index 000000000..a125e147c --- /dev/null +++ b/lib/esbonio/esbonio/server/features/rst/directives.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import typing + +from lsprotocol import types + +from esbonio import server +from esbonio.server.features.directives import DirectiveFeature +from esbonio.server.features.directives import completion +from esbonio.sphinx_agent.types import RST_DIRECTIVE + +if typing.TYPE_CHECKING: + from typing import List + from typing import Optional + + +class RstDirectives(server.LanguageFeature): + """A frontend to directives for reStructuredText syntax.""" + + def __init__(self, directives: DirectiveFeature, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.directives = directives + self._insert_behavior = "replace" + + completion_trigger = server.CompletionTrigger( + patterns=[RST_DIRECTIVE], + languages={"rst"}, + characters={".", "`"}, + ) + + def initialized(self, params: types.InitializedParams): + """Called once the initial handshake between client and server has finished.""" + self.configuration.subscribe( + "esbonio.server.completion", + server.CompletionConfig, + self.update_configuration, + ) + + def update_configuration( + self, event: server.ConfigChangeEvent[server.CompletionConfig] + ): + """Called when the user's configuration is updated.""" + self._insert_behavior = event.value.preferred_insert_behavior + + async def completion( + self, context: server.CompletionContext + ) -> Optional[List[types.CompletionItem]]: + """Provide completion suggestions for directives.""" + + groups = context.match.groupdict() + + # Are we completing a directive's options? + if "directive" not in groups: + return await self.complete_options(context) + + # Don't offer completions for targets + if (groups["name"] or "").startswith("_"): + return None + + # 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 await self.complete_arguments(context) + + return await self.complete_directives(context) + + async def complete_options(self, context: server.CompletionContext): + return None + + async def complete_arguments(self, context: server.CompletionContext): + return None + + async def complete_directives( + self, context: server.CompletionContext + ) -> Optional[List[types.CompletionItem]]: + """Return completion suggestions for the available directives.""" + + render_func = completion.get_directive_renderer( + context.language, self._insert_behavior + ) + if render_func is None: + return None + + items = [] + for directive in await self.directives.suggest_directives(context): + if (item := render_func(context, directive)) is not None: + items.append(item) + + if len(items) > 0: + return items + + return None + + +def esbonio_setup(esbonio: server.EsbonioLanguageServer, directives: DirectiveFeature): + rst_directives = RstDirectives(directives, esbonio) + esbonio.add_feature(rst_directives) diff --git a/lib/esbonio/esbonio/server/features/rst/roles.py b/lib/esbonio/esbonio/server/features/rst/roles.py new file mode 100644 index 000000000..6c31d0499 --- /dev/null +++ b/lib/esbonio/esbonio/server/features/rst/roles.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +import typing + +from lsprotocol import types + +from esbonio import server +from esbonio.server.features.roles import RolesFeature +from esbonio.server.features.roles import completion +from esbonio.sphinx_agent.types import RST_DIRECTIVE +from esbonio.sphinx_agent.types import RST_ROLE + +if typing.TYPE_CHECKING: + from typing import List + from typing import Optional + + +class RstRoles(server.LanguageFeature): + """A frontend to roles for reStructuredText syntax.""" + + def __init__(self, roles: RolesFeature, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.roles = roles + self._insert_behavior = "replace" + + completion_trigger = server.CompletionTrigger( + patterns=[RST_ROLE], + languages={"rst"}, + characters={":", "`", "<", "/"}, + ) + + def initialized(self, params: types.InitializedParams): + """Called once the initial handshake between client and server has finished.""" + self.configuration.subscribe( + "esbonio.server.completion", + server.CompletionConfig, + self.update_configuration, + ) + + def update_configuration( + self, event: server.ConfigChangeEvent[server.CompletionConfig] + ): + """Called when the user's configuration is updated.""" + self._insert_behavior = event.value.preferred_insert_behavior + + async def completion( + self, context: server.CompletionContext + ) -> Optional[List[types.CompletionItem]]: + """Provide completion suggestions for roles.""" + + 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 await 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 await 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 await 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 RST_DIRECTIVE.match(line): + return [] + + return await self.complete_roles(context) + + async def complete_targets( + self, context: server.CompletionContext + ) -> Optional[List[types.CompletionItem]]: + """Provide completion suggestions for role targets.""" + + render_func = completion.get_role_target_renderer( + context.language, self._insert_behavior + ) + if render_func is None: + return None + + items = [] + role_name = context.match.group("name") + for target in await self.roles.suggest_targets(context, role_name): + if (item := render_func(context, target)) is not None: + items.append(item) + + return items if len(items) > 0 else None + + async def complete_roles( + self, context: server.CompletionContext + ) -> Optional[List[types.CompletionItem]]: + """Return completion suggestions for the available roles""" + + render_func = completion.get_role_renderer( + context.language, self._insert_behavior + ) + if render_func is None: + return None + + items = [] + for role in await self.roles.suggest_roles(context): + if (item := render_func(context, role)) is not None: + items.append(item) + + if len(items) > 0: + return items + + return None + + +def esbonio_setup(esbonio: server.EsbonioLanguageServer, roles: RolesFeature): + rst_roles = RstRoles(roles, esbonio) + esbonio.add_feature(rst_roles) 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 06fca953d..a8305e4b1 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py @@ -14,9 +14,9 @@ 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 esbonio.sphinx_agent import types from .client import ClientState from .config import SphinxConfig @@ -59,7 +59,7 @@ def __init__( *args, **kwargs, ): - super().__init__(protocol_cls=protocol_cls, *args, **kwargs) # type: ignore[misc] + super().__init__(*args, protocol_cls=protocol_cls, **kwargs) # type: ignore[misc] self.id = str(uuid4()) """The client's id.""" @@ -204,7 +204,6 @@ async def start(self) -> SphinxClient: params = types.CreateApplicationParams( command=self.config.build_command, - enable_sync_scrolling=self.config.enable_sync_scrolling, ) self.sphinx_info = await self.protocol.send_request_async( @@ -273,7 +272,7 @@ async def forward_stderr(server: asyncio.subprocess.Process): # EOF is signalled with an empty bytestring while (line := await server.stderr.readline()) != b"": - sphinx_logger.info(line.decode().strip()) + sphinx_logger.info(line.decode().rstrip()) def make_subprocess_sphinx_client( @@ -317,7 +316,7 @@ def make_test_sphinx_client(config: SphinxConfig) -> SubprocessSphinxClient: @client.feature("window/logMessage") def _(params): - print(params.message, file=sys.stderr) + print(params.message, file=sys.stderr) # noqa: T201 @client.feature("$/progress") def _on_progress(params): @@ -354,14 +353,19 @@ def get_start_command(config: SphinxConfig, logger: logging.Logger): 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) + result = subprocess.run( + ["command", "-v", "lsp-devtools"], # noqa: S607 + capture_output=True, + check=False, + ) + 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) + logger.debug("Unable to locate lsp-devtools command\n%s", 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 ba92489f0..855f53d38 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/config.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/config.py @@ -45,9 +45,6 @@ class SphinxConfig: enable_dev_tools: bool = attrs.field(default=False) """Flag to enable dev tools.""" - enable_sync_scrolling: bool = attrs.field(default=True) - """Flag to enable sync scrolling.""" - python_command: List[str] = attrs.field(factory=list) """The command to use when launching the python interpreter.""" @@ -106,7 +103,6 @@ def resolve( return SphinxConfig( enable_dev_tools=self.enable_dev_tools, - enable_sync_scrolling=self.enable_sync_scrolling, cwd=cwd, env_passthrough=self.env_passthrough, python_command=self.python_command, @@ -227,7 +223,7 @@ def _resolve_build_command(self, uri: Uri, logger: logging.Logger) -> List[str]: logger.debug("Trying path: %s", current) if conf_py.exists(): cache = platformdirs.user_cache_dir("esbonio", "swyddfa") - project = hashlib.md5(str(current).encode()).hexdigest() + project = hashlib.md5(str(current).encode()).hexdigest() # noqa: S324 build_dir = str(pathlib.Path(cache, project)) return ["sphinx-build", "-M", "dirhtml", str(current), str(build_dir)] diff --git a/lib/esbonio/esbonio/server/features/sphinx_support/roles.py b/lib/esbonio/esbonio/server/features/sphinx_support/roles.py index fcd4d6c7c..582b17d92 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_support/roles.py +++ b/lib/esbonio/esbonio/server/features/sphinx_support/roles.py @@ -1,36 +1,91 @@ from __future__ import annotations +import logging import typing -from lsprotocol import types +from lsprotocol import types as lsp from esbonio import server from esbonio.server.features import roles from esbonio.server.features.project_manager import ProjectManager +from esbonio.sphinx_agent import types if typing.TYPE_CHECKING: from typing import List from typing import Optional + from esbonio.server import Uri + from esbonio.server.features.project_manager import Project -class SphinxRoles(roles.RoleProvider): - """Support for roles in a sphinx project.""" - def __init__(self, manager: ProjectManager): +TARGET_KINDS = { + "attribute": lsp.CompletionItemKind.Field, + "doc": lsp.CompletionItemKind.File, + "class": lsp.CompletionItemKind.Class, + "envvar": lsp.CompletionItemKind.Variable, + "function": lsp.CompletionItemKind.Function, + "method": lsp.CompletionItemKind.Method, + "module": lsp.CompletionItemKind.Module, + "term": lsp.CompletionItemKind.Text, +} + + +class ObjectsProvider(roles.RoleTargetProvider): + """Expose domain objects as potential role targets""" + + def __init__(self, logger: logging.Logger, manager: ProjectManager): self.manager = manager + self.logger = logger - async def suggest_roles( - self, context: server.CompletionContext - ) -> Optional[List[roles.Role]]: - """Given a completion context, suggest roles that may be used.""" + async def suggest_targets( # type: ignore[override] + self, + context: server.CompletionContext, + *, + obj_types: List[str], + ) -> Optional[List[lsp.CompletionItem]]: + self.logger.debug("Suggesting targets for types: %s", obj_types) if (project := self.manager.get_project(context.uri)) is None: return None + db = await project.get_db() + query = " ".join( + [ + "SELECT name, display, objtype FROM objects", + "WHERE printf('%s:%s', domain, objtype) IN (", + ", ".join("?" for _ in range(len(obj_types))), + ");", + ] + ) + + items = [] + cursor = await db.execute(query, tuple(obj_types)) + for name, display, type_ in await cursor.fetchall(): + kind = TARGET_KINDS.get(type_, lsp.CompletionItemKind.Reference) + items.append( + lsp.CompletionItem( + label=name, + detail=display, + kind=kind, + ), + ) + + return items + + +class SphinxRoles(roles.RoleProvider): + """Support for roles in a sphinx project.""" + + def __init__(self, manager: ProjectManager): + self.manager = manager + + async def get_default_domain(self, project: Project, uri: Uri) -> str: + """Get the name of the default domain for the given document""" + # Does the document have a default domain set? results = await project.find_symbols( - uri=str(context.uri.resolve()), - kind=types.SymbolKind.Class.value, + uri=str(uri.resolve()), + kind=lsp.SymbolKind.Class.value, detail="default-domain", ) if len(results) > 0: @@ -39,32 +94,63 @@ async def suggest_roles( default_domain = None primary_domain = await project.get_config_value("primary_domain") - active_domain = default_domain or primary_domain or "py" + return default_domain or primary_domain or "py" + + async def get_role(self, uri: Uri, name: str) -> Optional[types.Role]: + """Return the role with the given name.""" + + if (project := self.manager.get_project(uri)) is None: + return None + + if (role := await project.get_role(name)) is not None: + return role + + if (role := await project.get_role(f"std:{name}")) is not None: + return role + + default_domain = await self.get_default_domain(project, uri) + return await project.get_role(f"{default_domain}:{name}") - result: List[roles.Role] = [] + async def suggest_roles( + self, context: server.CompletionContext + ) -> Optional[List[types.Role]]: + """Given a completion context, suggest roles that may be used.""" + + if (project := self.manager.get_project(context.uri)) is None: + return None + + default_domain = await self.get_default_domain(project, context.uri) + + result: List[types.Role] = [] for name, implementation in await project.get_roles(): # std: directives can be used unqualified if name.startswith("std:"): short_name = name.replace("std:", "") result.append( - roles.Role(name=short_name, implementation=implementation) + types.Role(name=short_name, implementation=implementation) ) # Also suggest unqualified versions of directives from the currently active domain. - elif name.startswith(f"{active_domain}:"): - short_name = name.replace(f"{active_domain}:", "") + elif name.startswith(f"{default_domain}:"): + short_name = name.replace(f"{default_domain}:", "") result.append( - roles.Role(name=short_name, implementation=implementation) + types.Role(name=short_name, implementation=implementation) ) - result.append(roles.Role(name=name, implementation=implementation)) + result.append(types.Role(name=name, implementation=implementation)) return result def esbonio_setup( + esbonio: server.EsbonioLanguageServer, project_manager: ProjectManager, roles_feature: roles.RolesFeature, ): - provider = SphinxRoles(project_manager) - roles_feature.add_provider(provider) + role_provider = SphinxRoles(project_manager) + obj_provider = ObjectsProvider( + esbonio.logger.getChild("ObjectsProvider"), project_manager + ) + + roles_feature.add_role_provider(role_provider) + roles_feature.add_target_provider("objects", obj_provider) diff --git a/lib/esbonio/esbonio/server/features/sphinx_support/symbols.py b/lib/esbonio/esbonio/server/features/sphinx_support/symbols.py index c50413ad7..0d39b6a05 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_support/symbols.py +++ b/lib/esbonio/esbonio/server/features/sphinx_support/symbols.py @@ -77,7 +77,7 @@ async def workspace_symbol( uri = Uri.parse(uri_str) range_ = self.converter.structure(json.loads(range_json), types.Range) - if detail != "" and name != detail: + if detail not in {"", name}: display_name = f"{name} {detail}" else: display_name = name diff --git a/lib/esbonio/esbonio/server/server.py b/lib/esbonio/esbonio/server/server.py index e41a6bb91..e90754a9d 100644 --- a/lib/esbonio/esbonio/server/server.py +++ b/lib/esbonio/esbonio/server/server.py @@ -174,8 +174,8 @@ def load_extension(self, name: str, setup: Callable): from esbonio.lsp.roles import Roles from esbonio.lsp.sphinx import SphinxLanguageServer - def esbonio_setup(rst: SphinxLanguageServer, roles: Roles): - ... + + 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 @@ -447,7 +447,7 @@ def _get_setup_arguments( args[name] = server continue - from .feature import LanguageFeature # noqa: F402 + from .feature import LanguageFeature if issubclass(type_, LanguageFeature): # Try and obtain an instance of the requested language feature. diff --git a/lib/esbonio/esbonio/server/setup.py b/lib/esbonio/esbonio/server/setup.py index edca13a11..38f10cb2f 100644 --- a/lib/esbonio/esbonio/server/setup.py +++ b/lib/esbonio/esbonio/server/setup.py @@ -8,12 +8,12 @@ from typing import Dict from typing import Iterable from typing import List +from typing import Set from typing import Type from lsprotocol import types from . import Uri -from .feature import CompletionContext if typing.TYPE_CHECKING: from .server import EsbonioLanguageServer @@ -39,10 +39,13 @@ def create_language_server( for module in modules: _load_module(server, module) - return _configure_lsp_methods(server) + _configure_lsp_methods(server) + _configure_completion(server) + + return server -def _configure_lsp_methods(server: EsbonioLanguageServer) -> EsbonioLanguageServer: +def _configure_lsp_methods(server: EsbonioLanguageServer): """Configure method handlers for the portions of the LSP spec we support.""" @server.feature(types.INITIALIZE) @@ -90,81 +93,6 @@ async def on_document_save( await call_features(ls, "document_save", params) - @server.feature( - types.TEXT_DOCUMENT_COMPLETION, - types.CompletionOptions( - trigger_characters=[">", ".", ":", "`", "<", "/", "{", "}"], - resolve_provider=True, - ), - ) - async def on_completion(ls: EsbonioLanguageServer, params: types.CompletionParams): - uri = params.text_document.uri - pos = params.position - doc = ls.workspace.get_text_document(uri) - - try: - line = doc.lines[pos.line] - except IndexError: - line = "" - - items = [] - - for cls, feature in ls: - 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 not (start <= pos.character <= stop): - continue - - context = CompletionContext( - uri=Uri.parse(uri), - doc=doc, - match=match, - position=pos, - capabilities=ls.client_capabilities, - ) - - ls.logger.debug("%s", context) - name = f"{cls.__name__}" - - try: - result = feature.completion(context) - if inspect.isawaitable(result): - result = await result - except Exception: - ls.logger.error( - "Error in '%s.complete' handler", name, exc_info=True - ) - continue - - for item in result or []: - item.data = {"source_feature": name, **(item.data or {})} # type: ignore - items.append(item) - - if len(items) > 0: - return types.CompletionList(is_incomplete=False, items=items) - - @server.feature(types.COMPLETION_ITEM_RESOLVE) - def on_completion_resolve( - ls: EsbonioLanguageServer, item: types.CompletionItem - ) -> types.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) - return item - @server.feature( types.TEXT_DOCUMENT_DIAGNOSTIC, types.DiagnosticOptions( @@ -248,7 +176,81 @@ async def on_did_change_watched_files( paths = [pathlib.Path(Uri.parse(event.uri)) for event in params.changes] await ls.configuration.update_file_configuration(paths) - return server + +def _configure_completion(server: EsbonioLanguageServer): + """Configuration completion handlers.""" + + trigger_characters: Set[str] = set() + + for _, feature in server: + if feature.completion_trigger is None: + continue + + trigger_characters.update(feature.completion_trigger.characters) + + @server.feature( + types.TEXT_DOCUMENT_COMPLETION, + types.CompletionOptions( + trigger_characters=list(trigger_characters), + resolve_provider=True, + ), + ) + async def on_completion(ls: EsbonioLanguageServer, params: types.CompletionParams): + uri = params.text_document.uri + pos = params.position + doc = ls.workspace.get_text_document(uri) + language = ls.get_language_at(doc, pos) + + items = [] + + for cls, feature in ls: + if not feature.completion_trigger: + continue + + context = feature.completion_trigger( + uri=Uri.parse(uri), + params=params, + document=doc, + language=language, + client_capabilities=ls.client_capabilities, + ) + + if context is None: + continue + + ls.logger.debug("%s", context) + name = f"{cls.__name__}" + + try: + result = feature.completion(context) + if inspect.isawaitable(result): + result = await result + except Exception: + ls.logger.exception("Error in '%s.complete' handler", name) + continue + + for item in result or []: + item.data = {"source_feature": name, **(item.data or {})} # type: ignore + items.append(item) + + if len(items) > 0: + return types.CompletionList(is_incomplete=False, items=items) + + @server.feature(types.COMPLETION_ITEM_RESOLVE) + def on_completion_resolve( + ls: EsbonioLanguageServer, item: types.CompletionItem + ) -> types.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) + return item async def call_features(ls: EsbonioLanguageServer, method: str, *args, **kwargs): diff --git a/lib/esbonio/esbonio/sphinx_agent/__main__.py b/lib/esbonio/esbonio/sphinx_agent/__main__.py index 251925fb5..8ac08d4c0 100644 --- a/lib/esbonio/esbonio/sphinx_agent/__main__.py +++ b/lib/esbonio/esbonio/sphinx_agent/__main__.py @@ -1,8 +1,5 @@ import asyncio -try: - from esbonio.sphinx_agent.server import main -except ImportError: - from .server import main +from .server import main asyncio.run(main()) diff --git a/lib/esbonio/esbonio/sphinx_agent/app.py b/lib/esbonio/esbonio/sphinx_agent/app.py index 90bfdff38..2b3e65bb7 100644 --- a/lib/esbonio/esbonio/sphinx_agent/app.py +++ b/lib/esbonio/esbonio/sphinx_agent/app.py @@ -2,22 +2,36 @@ import logging import pathlib -from typing import IO +import typing 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 . import types from .database import Database from .log import DiagnosticFilter +if typing.TYPE_CHECKING: + from typing import IO + 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 Type + + from sphinx.domains import Domain + + RoleDefinition = Tuple[str, Any, List[types.Role.TargetProvider]] + 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) @@ -38,12 +52,52 @@ def __init__(self, dbpath: pathlib.Path, app: _Sphinx): self.db = Database(dbpath) self.log = DiagnosticFilter(app) - # Override sphinx's usual logging setup function - sphinx_logging_module.setup = setup_logging # type: ignore + self._roles: List[RoleDefinition] = [] + """Roles captured during Sphinx startup.""" + + def add_role( + self, + name: str, + role: Any, + target_providers: Optional[List[types.Role.TargetProvider]] = None, + ): + """Register a role with esbonio. + + Parameters + ---------- + name + The name of the role, as the user would type in a document + + role + The role's implementation + + target_providers + A list of target providers for the role + """ + self._roles.append((name, role, target_providers or [])) + + @staticmethod + def create_role_target_provider(name: str, **kwargs) -> types.Role.TargetProvider: + """Create a new role target provider + + Parameters + ---------- + name + The name of the provider + + kwargs + Additional arguments to pass to the provider instance + + Returns + ------- + types.Role.TargetProvider + The target provider + """ + return types.Role.TargetProvider(name, kwargs) class Sphinx(_Sphinx): - """A regular sphinx application with a few extra fields.""" + """An extended sphinx application that integrates with esbonio.""" esbonio: Esbonio @@ -51,9 +105,38 @@ def __init__(self, *args, **kwargs): # Disable color codes console.nocolor() + # Add in esbonio specific functionality self.esbonio = Esbonio( dbpath=pathlib.Path(kwargs["outdir"], "esbonio.db").resolve(), app=self, ) + # Override sphinx's usual logging setup function + sphinx_logging_module.setup = setup_logging # type: ignore + super().__init__(*args, **kwargs) + + def add_role(self, name: str, role: Any, override: bool = False): + super().add_role(name, role, override) + self.esbonio.add_role(name, role) + + def add_domain(self, domain: Type[Domain], override: bool = False) -> None: + super().add_domain(domain, override) + + target_types: Dict[str, Set[str]] = {} + + for obj_name, item_type in domain.object_types.items(): + for role_name in item_type.roles: + target_type = f"{domain.name}:{obj_name}" + target_types.setdefault(role_name, set()).add(target_type) + + for name, role in domain.roles.items(): + providers = [] + if (item_types := target_types.get(name)) is not None: + providers.append( + self.esbonio.create_role_target_provider( + "objects", obj_types=list(item_types) + ) + ) + + self.esbonio.add_role(f"{domain.name}:{name}", role, providers) diff --git a/lib/esbonio/esbonio/sphinx_agent/database.py b/lib/esbonio/esbonio/sphinx_agent/database.py index d794db924..80be9e26f 100644 --- a/lib/esbonio/esbonio/sphinx_agent/database.py +++ b/lib/esbonio/esbonio/sphinx_agent/database.py @@ -88,7 +88,7 @@ def clear_table(self, table: Table, **kwargs): """ # TODO: Is there a way to pass the table name as a '?' parameter? - base_query = f"DELETE FROM {table.name}" + base_query = f"DELETE FROM {table.name}" # noqa: S608 where: List[str] = [] parameters: List[Any] = [] @@ -140,5 +140,5 @@ def insert_values(self, table: Table, values: List[Tuple]): cursor = self.db.cursor() placeholder = "(" + ",".join(["?" for _ in range(len(values[0]))]) + ")" - cursor.executemany(f"INSERT INTO {table.name} VALUES {placeholder}", values) + cursor.executemany(f"INSERT INTO {table.name} VALUES {placeholder}", values) # noqa: S608 self.db.commit() diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py b/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py index 8bf9f7297..b669b4a64 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py @@ -1,6 +1,5 @@ import inspect import logging -import os.path import pathlib import sys import traceback @@ -34,6 +33,7 @@ f"{__name__}.symbols", f"{__name__}.directives", f"{__name__}.roles", + f"{__name__}.domains", ) @@ -124,8 +124,7 @@ def create_sphinx_app(self, request: types.CreateApplicationRequest): # TODO: Sphinx 7.x has introduced a `include-read` event # See: https://github.com/sphinx-doc/sphinx/pull/11657 - if request.params.enable_sync_scrolling: - _enable_sync_scrolling(self.app) + _enable_sync_scrolling(self.app) response = types.CreateApplicationResponse( id=request.id, @@ -198,21 +197,11 @@ def _enable_sync_scrolling(app: Sphinx): """Given a Sphinx application, configure it so that we can support syncronised scrolling.""" - # On OSes like Fedora Silverblue where `/home` is a symlink for `/var/home` - # we could have a situation where `STATIC_DIR` and `app.confdir` have - # different root dirs... which is enough to cause `os.path.relpath` to return - # the wrong path. + # Inline the JS code we need to enable sync scrolling. # - # Fully resolving both `STATIC_DIR` and `app.confdir` should be enough to - # mitigate this. - confdir = pathlib.Path(app.confdir).resolve() + # Yes this "bloats" every page in the generated docs, but is generally more robust + # see: https://github.com/swyddfa/esbonio/issues/810 + webview_js = STATIC_DIR / "webview.js" + app.add_js_file(None, body=webview_js.read_text()) - # Push our folder of static assets into the user's project. - # Path needs to be relative to their project's confdir. - reldir = os.path.relpath(str(STATIC_DIR), start=str(confdir)) - app.config.html_static_path.append(reldir) - - app.add_js_file("webview.js") - - # Inject source line numbers into build output app.add_transform(LineNumberTransform) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/diagnostics.py b/lib/esbonio/esbonio/sphinx_agent/handlers/diagnostics.py index 70c47d1b3..cc7b60240 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/diagnostics.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/diagnostics.py @@ -43,7 +43,7 @@ def setup(app: Sphinx): app.connect("config-inited", init_db) app.connect("source-read", clear_diagnostics) - # TODO + # TODO: Support for Sphinx v7+ # app.connect("include-read") app.connect("build-finished", sync_diagnostics) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/directives.py b/lib/esbonio/esbonio/sphinx_agent/handlers/directives.py index c5b9b1b1c..5eb69399c 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/directives.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/directives.py @@ -70,8 +70,8 @@ def index_directives(app: Sphinx): ignored_directives = {"restructuredtext-test-directive"} found_directives = { - **docutils_directives._directive_registry, - **docutils_directives._directives, + **docutils_directives._directive_registry, # type: ignore[attr-defined] + **docutils_directives._directives, # type: ignore[attr-defined] } for name, directive in found_directives.items(): diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/domains.py b/lib/esbonio/esbonio/sphinx_agent/handlers/domains.py new file mode 100644 index 000000000..661f693e7 --- /dev/null +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/domains.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import typing + +from sphinx import addnodes + +from .. import types +from ..app import Database +from ..app import Sphinx +from ..util import as_json + +if typing.TYPE_CHECKING: + from typing import Dict + from typing import Optional + from typing import Tuple + +OBJECTS_TABLE = Database.Table( + "objects", + [ + Database.Column(name="name", dtype="TEXT"), + Database.Column(name="display", dtype="TEXT"), + Database.Column(name="domain", dtype="TEXT"), + Database.Column(name="objtype", dtype="TEXT"), + Database.Column(name="docname", dtype="TEXT"), + Database.Column(name="description", dtype="TEXT"), + Database.Column(name="location", dtype="JSON"), + ], +) + + +class DomainObjects: + """Discovers and indexes domain objects.""" + + def __init__(self, app: Sphinx): + self._info: Dict[ + Tuple[str, str, str, str], Tuple[Optional[str], Optional[str]] + ] = {} + + app.connect("builder-inited", self.init_db) + app.connect("object-description-transform", self.object_defined) + app.connect("build-finished", self.commit) + + def init_db(self, app: Sphinx): + """Prepare the database.""" + app.esbonio.db.ensure_table(OBJECTS_TABLE) + + def commit(self, app, exc): + """Commit changes to the database. + + The only way to guarantee we discover all objects, from all domains correctly, + is to call the ``get_objects()`` method on each domain. This means we process + every object, every time we build. + + I will be *very* surprised if this never becomes a performance issue, but we + will have to think of a smarter approach when it comes to it. + """ + app.esbonio.db.clear_table(OBJECTS_TABLE) + rows = [] + + for name, domain in app.env.domains.items(): + for objname, dispname, objtype, docname, _, _ in domain.get_objects(): + desc, location = self._info.get( + (objname, name, objtype, docname), (None, None) + ) + rows.append( + (objname, str(dispname), name, objtype, docname, desc, location) + ) + + app.esbonio.db.insert_values(OBJECTS_TABLE, rows) + self._info.clear() + + def object_defined( + self, app: Sphinx, domain: str, objtype: str, content: addnodes.desc_content + ): + """Record additional information about a domain object. + + Despite having a certain amount of structure to them (thanks to the API), + domains can still do arbitrary things - take a peek at the implementations of + the ``std``, ``py`` and ``cpp`` domains! + + So while this will never be perfect, this method is called each time the + ``object-description-transform`` event is fired and attempts to extract the + object's description and precise location. + + The trick however, is linking these items up with the correct object + """ + + sig = content.parent[0] + + try: + name = sig["ids"][0] # type: ignore[index] + except Exception: + return + + docname = app.env.docname + description = content.astext() + + if (source := sig.source) is not None and (line := sig.line) is not None: + location = as_json( + types.Location( + uri=str(types.Uri.for_file(source)), + range=types.Range( + start=types.Position(line=line, character=0), + end=types.Position(line=line + 1, character=0), + ), + ) + ) + else: + location = None + + key = (name, domain, objtype, docname) + self._info[key] = (description, location) + + +def setup(app: Sphinx): + DomainObjects(app) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/roles.py b/lib/esbonio/esbonio/sphinx_agent/handlers/roles.py index 40d83b685..1f96d33b0 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/roles.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/roles.py @@ -1,6 +1,6 @@ import inspect from typing import Any -from typing import List +from typing import Dict from typing import Optional from docutils.parsers.rst import roles as docutils_roles @@ -16,6 +16,7 @@ Database.Column(name="name", dtype="TEXT"), Database.Column(name="implementation", dtype="TEXT"), Database.Column(name="location", dtype="JSON"), + Database.Column(name="target_providers", dtype="JSON"), ], ) @@ -27,8 +28,8 @@ def get_impl_name(role: Any) -> str: return f"{role.__module__}.{role.__class__.__name__}" -def get_impl_location(impl: Any) -> Optional[str]: - """Get the implementation location of the given directive""" +def get_impl_location(impl: Any) -> Optional[types.Location]: + """Get the implementation location of the given role""" try: if (filepath := inspect.getsourcefile(impl)) is None: @@ -45,7 +46,7 @@ def get_impl_location(impl: Any) -> Optional[str]: ), ) - return as_json(location) + return location except Exception: # TODO: Log the error somewhere.. return None @@ -54,32 +55,31 @@ def get_impl_location(impl: Any) -> Optional[str]: def index_roles(app: Sphinx): """Index all the roles that are available to this app.""" - roles: List[types.Directive] = [] + roles: Dict[str, types.Role] = {} + + # Process the roles registered through Sphinx + for name, impl, providers in app.esbonio._roles: + roles[name] = types.Role(name, get_impl_name(impl), target_providers=providers) + + # Look any remaining docutils provided roles found_roles = { **docutils_roles._roles, # type: ignore[attr-defined] **docutils_roles._role_registry, # type: ignore[attr-defined] } for name, role in found_roles.items(): - if role == docutils_roles.unimplemented_role: + if role == docutils_roles.unimplemented_role or name in roles: continue - roles.append((name, get_impl_name(role), None)) - - for prefix, domain in app.env.domains.items(): - for name, role in domain.roles.items(): - roles.append( - ( - f"{prefix}:{name}", - get_impl_name(role), - None, - ) - ) + roles[name] = types.Role(name, get_impl_name(role)) app.esbonio.db.ensure_table(ROLES_TABLE) app.esbonio.db.clear_table(ROLES_TABLE) - app.esbonio.db.insert_values(ROLES_TABLE, roles) + app.esbonio.db.insert_values( + ROLES_TABLE, [r.to_db(as_json) for r in roles.values()] + ) def setup(app: Sphinx): - app.connect("builder-inited", index_roles) + # Ensure that this runs as late as possibile + app.connect("builder-inited", index_roles, priority=999) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/symbols.py b/lib/esbonio/esbonio/sphinx_agent/handlers/symbols.py index 4364686f1..258ef294a 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/symbols.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/symbols.py @@ -107,7 +107,7 @@ def astext(self): return self["text"] -def dummy_role(name, rawtext, text, lineno, inliner, options={}, content=[]): +def dummy_role(name, rawtext, text, lineno, inliner, options=None, content=None): node = a_role() node.line = lineno diff --git a/lib/esbonio/esbonio/sphinx_agent/static/webview.js b/lib/esbonio/esbonio/sphinx_agent/static/webview.js index 5bec76c4d..e9e3f6d91 100644 --- a/lib/esbonio/esbonio/sphinx_agent/static/webview.js +++ b/lib/esbonio/esbonio/sphinx_agent/static/webview.js @@ -1,4 +1,4 @@ -// This file gets injected into html pages built with Sphinx (assuming it's enabled of course!) +// This file gets injected into html pages built with Sphinx // which allows the webpage to talk with the preview server and coordinate details such as refreshes // and page scrolling. function indexScrollTargets() { diff --git a/lib/esbonio/esbonio/sphinx_agent/types.py b/lib/esbonio/esbonio/sphinx_agent/types/__init__.py similarity index 76% rename from lib/esbonio/esbonio/sphinx_agent/types.py rename to lib/esbonio/esbonio/sphinx_agent/types/__init__.py index 05d1104d2..47758c9fb 100644 --- a/lib/esbonio/esbonio/sphinx_agent/types.py +++ b/lib/esbonio/esbonio/sphinx_agent/types/__init__.py @@ -5,7 +5,6 @@ """ import dataclasses -import enum import os import pathlib import re @@ -17,6 +16,28 @@ from typing import Union from urllib import parse +from .lsp import Diagnostic +from .lsp import DiagnosticSeverity +from .lsp import Location +from .lsp import Position +from .lsp import Range +from .roles import MYST_ROLE +from .roles import RST_DEFAULT_ROLE +from .roles import RST_ROLE +from .roles import Role + +__all__ = ( + "Diagnostic", + "DiagnosticSeverity", + "Location", + "MYST_ROLE", + "Position", + "RST_DEFAULT_ROLE", + "RST_ROLE", + "Range", + "Role", +) + MYST_DIRECTIVE: "re.Pattern" = re.compile( r""" (\s*) # directives can be indented @@ -37,52 +58,6 @@ initial declaration. """ -MYST_ROLE: "re.Pattern" = re.compile( - r""" - ([^\w`]|^\s*) # roles cannot be preceeded by letter chars - (?P - { # roles start with a '{' - (?P[:\w-]+)? # roles have a name - }? # roles end with a '}' - ) - (?P - ` # targets begin with a '`' character - ((?P[^<`>]*?)<)? # targets may specify an alias - (?P[!~])? # targets may have a modifier - (?P