diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index aac2d51f9..b8f3d60ba 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,6 +7,9 @@ // "build": { // "dockerfile": "Dockerfile" // }, + "containerEnv": { + "TZ": "UTC" + }, // 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. diff --git a/.github/workflows/devenv.yml b/.github/workflows/devenv.yml index 3133a5c74..b14b2b7b9 100644 --- a/.github/workflows/devenv.yml +++ b/.github/workflows/devenv.yml @@ -49,7 +49,7 @@ jobs: - name: Build and push Docker image id: push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: .devcontainer/ push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/lsp-release.yml b/.github/workflows/lsp-release.yml index 8a5b3160a..4190beec9 100644 --- a/.github/workflows/lsp-release.yml +++ b/.github/workflows/lsp-release.yml @@ -14,7 +14,7 @@ jobs: lsp: ${{steps.check-lsp.outputs.build}} code: ${{steps.check-code.outputs.build}} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.gitignore b/.gitignore index be0dfda64..019c22875 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .env .ipynb_checkpoints .tox +.vscode/extensions/esbonio .vscode-test *.pyc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3c347e6a..ab3a205b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.7 + rev: v0.5.2 hooks: - id: ruff args: [--fix] @@ -17,7 +17,7 @@ repos: - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.10.0' + rev: 'v1.10.1' hooks: - id: mypy name: mypy (scripts) diff --git a/.vscode/settings.json b/.vscode/settings.json index 13097473d..9722740e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", + "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, @@ -12,9 +12,6 @@ "[typescript]": { "editor.tabSize": 2 }, - "files.associations": { - "setup.cfg": "ini", - }, "files.exclude": { "**/.git": true, "**/.svn": true, @@ -32,10 +29,6 @@ "**/.mypy_cache": true }, "esbonio.server.showDeprecationWarnings": true, - "isort.args": [ - "--settings-file", - "./lib/esbonio/pyproject.toml" - ], "python.testing.pytestArgs": [ "lib/esbonio/tests" ], diff --git a/code/Makefile b/code/Makefile index 227255abb..923c4b37b 100644 --- a/code/Makefile +++ b/code/Makefile @@ -2,7 +2,7 @@ include ../.devcontainer/tools.mk ESBONIO ?= --pre esbonio -.PHONY: dist dev-deps release-deps release +.PHONY: dist dev-deps release-deps release install watch: dev-deps $(NPM) -test -d dist && rm -r dist @@ -12,6 +12,10 @@ compile: dev-deps $(NPM) -test -d dist && rm -r dist $(NPM) run compile +install: compile + -test -d ../.vscode/extensions || mkdir -p ../.vscode/extensions + test -L ../.vscode/extensions/esbonio || ln -s $(PWD) ../.vscode/extensions/esbonio + dist: release-deps $(NPM) -test -d dist && rm -r dist $(NPM) run package diff --git a/code/changes/854.feature.md b/code/changes/854.feature.md new file mode 100644 index 000000000..89329bcc6 --- /dev/null +++ b/code/changes/854.feature.md @@ -0,0 +1,4 @@ +Add a "Sphinx Processes" tree view + +The tree view shows the current status of Sphinx sub-processes as well as providing a place from which to control them. +Currently there is just one command that can be used to restart a given process diff --git a/code/changes/854.misc.md b/code/changes/854.misc.md new file mode 100644 index 000000000..b5f4fb078 --- /dev/null +++ b/code/changes/854.misc.md @@ -0,0 +1 @@ +Removed Language Status Items diff --git a/code/changes/857.misc.md b/code/changes/857.misc.md new file mode 100644 index 000000000..408e19b3f --- /dev/null +++ b/code/changes/857.misc.md @@ -0,0 +1 @@ +Update bundled version of the language server to `v1.0.0b6` diff --git a/code/package-lock.json b/code/package-lock.json index a6d203170..202259059 100644 --- a/code/package-lock.json +++ b/code/package-lock.json @@ -1,12 +1,12 @@ { "name": "esbonio", - "version": "0.94.0", + "version": "0.94.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "esbonio", - "version": "0.94.0", + "version": "0.94.2", "license": "MIT", "dependencies": { "@vscode/python-extension": "^1.0.5", @@ -17,10 +17,10 @@ "@types/glob": "^8.1.0", "@types/node": "^18", "@types/vscode": "1.78.0", - "@vscode/vsce": "^2.26.1", - "esbuild": "^0.21.4", + "@vscode/vsce": "^2.30.0", + "esbuild": "^0.23.0", "ovsx": "^0.9.1", - "typescript": "^5.4.5" + "typescript": "^5.5.3" }, "engines": { "vscode": "^1.82.0" @@ -235,9 +235,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.4.tgz", - "integrity": "sha512-Zrm+B33R4LWPLjDEVnEqt2+SLTATlru1q/xYKVn8oVTbiRBGmK2VIMoIYGJDGyftnGaC788IuzGFAlb7IQ0Y8A==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", "cpu": [ "ppc64" ], @@ -247,13 +247,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", "cpu": [ "arm" ], @@ -263,13 +263,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", "cpu": [ "arm64" ], @@ -279,13 +279,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.4.tgz", - "integrity": "sha512-mDqmlge3hFbEPbCWxp4fM6hqq7aZfLEHZAKGP9viq9wMUBVQx202aDIfc3l+d2cKhUJM741VrCXEzRFhPDKH3Q==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", "cpu": [ "x64" ], @@ -295,13 +295,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", "cpu": [ "arm64" ], @@ -311,13 +311,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.4.tgz", - "integrity": "sha512-uBsuwRMehGmw1JC7Vecu/upOjTsMhgahmDkWhGLWxIgUn2x/Y4tIwUZngsmVb6XyPSTXJYS4YiASKPcm9Zitag==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", "cpu": [ "x64" ], @@ -327,13 +327,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.4.tgz", - "integrity": "sha512-8JfuSC6YMSAEIZIWNL3GtdUT5NhUA/CMUCpZdDRolUXNAXEE/Vbpe6qlGLpfThtY5NwXq8Hi4nJy4YfPh+TwAg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", "cpu": [ "arm64" ], @@ -343,13 +343,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.4.tgz", - "integrity": "sha512-8d9y9eQhxv4ef7JmXny7591P/PYsDFc4+STaxC1GBv0tMyCdyWfXu2jBuqRsyhY8uL2HU8uPyscgE2KxCY9imQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", "cpu": [ "x64" ], @@ -359,13 +359,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.4.tgz", - "integrity": "sha512-2rqFFefpYmpMs+FWjkzSgXg5vViocqpq5a1PSRgT0AvSgxoXmGF17qfGAzKedg6wAwyM7UltrKVo9kxaJLMF/g==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", "cpu": [ "arm" ], @@ -375,13 +375,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.4.tgz", - "integrity": "sha512-/GLD2orjNU50v9PcxNpYZi+y8dJ7e7/LhQukN3S4jNDXCKkyyiyAz9zDw3siZ7Eh1tRcnCHAo/WcqKMzmi4eMQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", "cpu": [ "arm64" ], @@ -391,13 +391,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", "cpu": [ "ia32" ], @@ -407,13 +407,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.4.tgz", - "integrity": "sha512-cSD2gzCK5LuVX+hszzXQzlWya6c7hilO71L9h4KHwqI4qeqZ57bAtkgcC2YioXjsbfAv4lPn3qe3b00Zt+jIfQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", "cpu": [ "loong64" ], @@ -423,13 +423,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.4.tgz", - "integrity": "sha512-qtzAd3BJh7UdbiXCrg6npWLYU0YpufsV9XlufKhMhYMJGJCdfX/G6+PNd0+v877X1JG5VmjBLUiFB0o8EUSicA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", "cpu": [ "mips64el" ], @@ -439,13 +439,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", "cpu": [ "ppc64" ], @@ -455,13 +455,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.4.tgz", - "integrity": "sha512-Y5AgOuVzPjQdgU59ramLoqSSiXddu7F3F+LI5hYy/d1UHN7K5oLzYBDZe23QmQJ9PIVUXwOdKJ/jZahPdxzm9w==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", "cpu": [ "riscv64" ], @@ -471,13 +471,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", "cpu": [ "s390x" ], @@ -487,13 +487,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.4.tgz", - "integrity": "sha512-Td9jv782UMAFsuLZINfUpoF5mZIbAj+jv1YVtE58rFtfvoKRiKSkRGQfHTgKamLVT/fO7203bHa3wU122V/Bdg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", "cpu": [ "x64" ], @@ -503,13 +503,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", "cpu": [ "x64" ], @@ -519,13 +519,29 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.4.tgz", - "integrity": "sha512-IsUmQeCY0aU374R82fxIPu6vkOybWIMc3hVGZ3ChRwL9hA1TwY+tS0lgFWV5+F1+1ssuvvXt3HFqe8roCip8Hg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", "cpu": [ "x64" ], @@ -535,13 +551,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.4.tgz", - "integrity": "sha512-hsKhgZ4teLUaDA6FG/QIu2q0rI6I36tZVfM4DBZv3BG0mkMIdEnMbhc4xwLvLJSS22uWmaVkFkqWgIS0gPIm+A==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", "cpu": [ "x64" ], @@ -551,13 +567,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.4.tgz", - "integrity": "sha512-UUfMgMoXPoA/bvGUNfUBFLCh0gt9dxZYIx9W4rfJr7+hKe5jxxHmfOK8YSH4qsHLLN4Ck8JZ+v7Q5fIm1huErg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", "cpu": [ "arm64" ], @@ -567,13 +583,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.4.tgz", - "integrity": "sha512-yIxbspZb5kGCAHWm8dexALQ9en1IYDfErzjSEq1KzXFniHv019VT3mNtTK7t8qdy4TwT6QYHI9sEZabONHg+aw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", "cpu": [ "ia32" ], @@ -583,13 +599,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.4.tgz", - "integrity": "sha512-sywLRD3UK/qRJt0oBwdpYLBibk7KiRfbswmWRDabuncQYSlf8aLEEUor/oP6KRz8KEG+HoiVLBhPRD5JWjS8Sg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", "cpu": [ "x64" ], @@ -599,7 +615,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@types/glob": { @@ -640,12 +656,13 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.26.1", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.26.1.tgz", - "integrity": "sha512-QOG6Ht7V93nhwcBxPWcG33UK0qDGEoJdg0xtVeaTN27W6PGdMJUJGTPhB/sNHUIFKwvwzv/zMAHvDgMNXbcwlA==", + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.30.0.tgz", + "integrity": "sha512-MBYpXdCY1SCdc2u/y11kmJuSODKFyZRpeRTQq5p4rSg05QSjSy5pz6h/BGLNdSahgXfKRBATEkjAcJFdJuDz8Q==", "dev": true, "dependencies": { "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", "azure-devops-node-api": "^12.5.0", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", @@ -679,6 +696,141 @@ "keytar": "^7.7.0" } }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.4.tgz", + "integrity": "sha512-0uL32egStKYfy60IqnynAChMTbL0oqpqk0Ew0YHiIb+fayuGZWADuIPHWUcY1GCnAA+VgchOPDMxnc2R3XGWEA==", + "dev": true, + "hasInstallScript": true, + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -1248,41 +1400,42 @@ } }, "node_modules/esbuild": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.4.tgz", - "integrity": "sha512-sFMcNNrj+Q0ZDolrp5pDhH0nRPN9hLIM3fRPwgbLYJeSHHgnXSnbV3xYgSVuOeLWH9c73VwmEverVzupIv5xuA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@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" + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" } }, "node_modules/escape-string-regexp": { @@ -2603,9 +2756,9 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2940,163 +3093,170 @@ } }, "@esbuild/aix-ppc64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.4.tgz", - "integrity": "sha512-Zrm+B33R4LWPLjDEVnEqt2+SLTATlru1q/xYKVn8oVTbiRBGmK2VIMoIYGJDGyftnGaC788IuzGFAlb7IQ0Y8A==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.4.tgz", - "integrity": "sha512-mDqmlge3hFbEPbCWxp4fM6hqq7aZfLEHZAKGP9viq9wMUBVQx202aDIfc3l+d2cKhUJM741VrCXEzRFhPDKH3Q==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.4.tgz", - "integrity": "sha512-uBsuwRMehGmw1JC7Vecu/upOjTsMhgahmDkWhGLWxIgUn2x/Y4tIwUZngsmVb6XyPSTXJYS4YiASKPcm9Zitag==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.4.tgz", - "integrity": "sha512-8JfuSC6YMSAEIZIWNL3GtdUT5NhUA/CMUCpZdDRolUXNAXEE/Vbpe6qlGLpfThtY5NwXq8Hi4nJy4YfPh+TwAg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.4.tgz", - "integrity": "sha512-8d9y9eQhxv4ef7JmXny7591P/PYsDFc4+STaxC1GBv0tMyCdyWfXu2jBuqRsyhY8uL2HU8uPyscgE2KxCY9imQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.4.tgz", - "integrity": "sha512-2rqFFefpYmpMs+FWjkzSgXg5vViocqpq5a1PSRgT0AvSgxoXmGF17qfGAzKedg6wAwyM7UltrKVo9kxaJLMF/g==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.4.tgz", - "integrity": "sha512-/GLD2orjNU50v9PcxNpYZi+y8dJ7e7/LhQukN3S4jNDXCKkyyiyAz9zDw3siZ7Eh1tRcnCHAo/WcqKMzmi4eMQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.4.tgz", - "integrity": "sha512-cSD2gzCK5LuVX+hszzXQzlWya6c7hilO71L9h4KHwqI4qeqZ57bAtkgcC2YioXjsbfAv4lPn3qe3b00Zt+jIfQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.4.tgz", - "integrity": "sha512-qtzAd3BJh7UdbiXCrg6npWLYU0YpufsV9XlufKhMhYMJGJCdfX/G6+PNd0+v877X1JG5VmjBLUiFB0o8EUSicA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.4.tgz", - "integrity": "sha512-Y5AgOuVzPjQdgU59ramLoqSSiXddu7F3F+LI5hYy/d1UHN7K5oLzYBDZe23QmQJ9PIVUXwOdKJ/jZahPdxzm9w==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.4.tgz", - "integrity": "sha512-Td9jv782UMAFsuLZINfUpoF5mZIbAj+jv1YVtE58rFtfvoKRiKSkRGQfHTgKamLVT/fO7203bHa3wU122V/Bdg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "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==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.4.tgz", - "integrity": "sha512-IsUmQeCY0aU374R82fxIPu6vkOybWIMc3hVGZ3ChRwL9hA1TwY+tS0lgFWV5+F1+1ssuvvXt3HFqe8roCip8Hg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.4.tgz", - "integrity": "sha512-hsKhgZ4teLUaDA6FG/QIu2q0rI6I36tZVfM4DBZv3BG0mkMIdEnMbhc4xwLvLJSS22uWmaVkFkqWgIS0gPIm+A==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.4.tgz", - "integrity": "sha512-UUfMgMoXPoA/bvGUNfUBFLCh0gt9dxZYIx9W4rfJr7+hKe5jxxHmfOK8YSH4qsHLLN4Ck8JZ+v7Q5fIm1huErg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.4.tgz", - "integrity": "sha512-yIxbspZb5kGCAHWm8dexALQ9en1IYDfErzjSEq1KzXFniHv019VT3mNtTK7t8qdy4TwT6QYHI9sEZabONHg+aw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.4.tgz", - "integrity": "sha512-sywLRD3UK/qRJt0oBwdpYLBibk7KiRfbswmWRDabuncQYSlf8aLEEUor/oP6KRz8KEG+HoiVLBhPRD5JWjS8Sg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", "dev": true, "optional": true }, @@ -3134,12 +3294,13 @@ "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==" }, "@vscode/vsce": { - "version": "2.26.1", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.26.1.tgz", - "integrity": "sha512-QOG6Ht7V93nhwcBxPWcG33UK0qDGEoJdg0xtVeaTN27W6PGdMJUJGTPhB/sNHUIFKwvwzv/zMAHvDgMNXbcwlA==", + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.30.0.tgz", + "integrity": "sha512-MBYpXdCY1SCdc2u/y11kmJuSODKFyZRpeRTQq5p4rSg05QSjSy5pz6h/BGLNdSahgXfKRBATEkjAcJFdJuDz8Q==", "dev": true, "requires": { "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", "azure-devops-node-api": "^12.5.0", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", @@ -3165,6 +3326,86 @@ "yazl": "^2.2.2" } }, + "@vscode/vsce-sign": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.4.tgz", + "integrity": "sha512-0uL32egStKYfy60IqnynAChMTbL0oqpqk0Ew0YHiIb+fayuGZWADuIPHWUcY1GCnAA+VgchOPDMxnc2R3XGWEA==", + "dev": true, + "requires": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "dev": true, + "optional": true + }, "agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -3595,34 +3836,35 @@ "dev": true }, "esbuild": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.4.tgz", - "integrity": "sha512-sFMcNNrj+Q0ZDolrp5pDhH0nRPN9hLIM3fRPwgbLYJeSHHgnXSnbV3xYgSVuOeLWH9c73VwmEverVzupIv5xuA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", "dev": true, "requires": { - "@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" + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" } }, "escape-string-regexp": { @@ -4651,9 +4893,9 @@ } }, "typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true }, "uc.micro": { diff --git a/code/package.json b/code/package.json index 9aa67a073..8f109bc77 100644 --- a/code/package.json +++ b/code/package.json @@ -24,7 +24,7 @@ "package": "vsce package --pre-release --baseImagesUrl https://github.com/swyddfa/esbonio/raw/release/code/", "vscode:prepublish": "npm run compile" }, - "main": "dist/node/extension", + "main": "dist/node/extension.js", "extensionDependencies": [ "ms-python.python", "chrisjsewell.myst-tml-syntax" @@ -38,10 +38,10 @@ "@types/glob": "^8.1.0", "@types/node": "^18", "@types/vscode": "1.78.0", - "@vscode/vsce": "^2.26.1", - "esbuild": "^0.21.4", + "@vscode/vsce": "^2.30.0", + "esbuild": "^0.23.0", "ovsx": "^0.9.1", - "typescript": "^5.4.5" + "typescript": "^5.5.3" }, "engines": { "vscode": "^1.82.0" @@ -78,7 +78,14 @@ { "command": "esbonio.server.restart", "title": "Restart Language Server", - "category": "Esbonio" + "category": "Esbonio", + "icon": "$(refresh)" + }, + { + "command": "esbonio.sphinx.restart", + "title": "Restart Sphinx Process", + "category": "Esbonio", + "icon": "$(refresh)" } ], "configuration": [ @@ -415,6 +422,28 @@ "group": "navigation", "when": "resourceLangId == restructuredtext || resourceLangId == markdown" } + ], + "view/title": [ + { + "command": "esbonio.server.restart", + "when": "view == sphinxProcesses", + "group": "navigation" + } + ], + "view/item/context": [ + { + "command": "esbonio.sphinx.restart", + "when": "view == sphinxProcesses && viewItem == process", + "group": "inline" + } + ] + }, + "views": { + "explorer": [ + { + "id": "sphinxProcesses", + "name": "Sphinx Processes" + } ] }, "walkthroughs": [ diff --git a/code/src/node/client.ts b/code/src/node/client.ts index 80463d9f2..6ef3a40a5 100644 --- a/code/src/node/client.ts +++ b/code/src/node/client.ts @@ -306,8 +306,10 @@ export class EsbonioClient { } - public scrollView(line: number) { - this.client?.sendNotification(Notifications.VIEW_SCROLL, { line: line }) + public scrollView(uri: vscode.Uri, line: number) { + this.client?.sendNotification(Notifications.VIEW_SCROLL, { + uri: uri.toString(), line: line + }) } @@ -368,7 +370,7 @@ export class EsbonioClient { }, window: { showDocument: async (params: ShowDocumentParams, next) => { - this.logger.debug(`window/showDocument: ${JSON.stringify(params, undefined, 2)}`) + // this.logger.debug(`window/showDocument: ${JSON.stringify(params, undefined, 2)}`) this.callHandlers("window/showDocument", { params: params, default: next }) return { success: true } } diff --git a/code/src/node/extension.ts b/code/src/node/extension.ts index 99437a18d..31d70ed3f 100644 --- a/code/src/node/extension.ts +++ b/code/src/node/extension.ts @@ -5,7 +5,7 @@ import { OutputChannelLogger } from '../common/log' import { PythonManager } from './python' import { PreviewManager } from "./preview"; import { EsbonioClient } from './client' -import { StatusManager } from './status'; +import { SphinxProcessProvider } from "./processTreeView"; let esbonio: EsbonioClient let logger: OutputChannelLogger @@ -21,7 +21,9 @@ export async function activate(context: vscode.ExtensionContext) { esbonio = new EsbonioClient(logger, pythonManager, context, channel) let previewManager = new PreviewManager(logger, context, esbonio) - let statusManager = new StatusManager(logger, context, esbonio) + context.subscriptions.push(vscode.window.registerTreeDataProvider( + 'sphinxProcesses', new SphinxProcessProvider(esbonio) + )); let config = vscode.workspace.getConfiguration("esbonio.server") if (config.get("enabled")) { diff --git a/code/src/node/preview.ts b/code/src/node/preview.ts index ff91f966e..2eb2e8296 100644 --- a/code/src/node/preview.ts +++ b/code/src/node/preview.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode' import { OutputChannelLogger } from '../common/log' import { EsbonioClient } from './client' import { Commands, Events, Notifications } from '../common/constants' -import { ShowDocumentParams } from 'vscode-languageclient' +import { ShowDocumentParams, Range } from 'vscode-languageclient' interface PreviewFileParams { uri: string @@ -17,9 +17,12 @@ export class PreviewManager { private panel?: vscode.WebviewPanel - // The uri of the document currently shown in the preview pane + /** The uri of the document currently shown in the preview pane */ private currentUri?: vscode.Uri + /** If `true`, indicates that we are currently changing the document being previewed */ + private changingDocument = false + constructor( private logger: OutputChannelLogger, private context: vscode.ExtensionContext, @@ -43,12 +46,6 @@ export class PreviewManager { (params: { params: ShowDocumentParams, default: any }) => this.showDocument(params) ) - // View -> editor sync scrolling implementation - client.addHandler( - Notifications.SCROLL_EDITOR, - (params: { line: number }) => { this.scrollEditor(params) } - ) - client.addHandler( Events.SERVER_START, async (_: any) => { @@ -80,21 +77,6 @@ export class PreviewManager { return await this.previewEditor(editor, vscode.ViewColumn.Beside) } - private scrollEditor(params: { line: number }) { - let editor = findEditorFor(this.currentUri) - if (!editor) { - return - } - // this.logger.debug(`Scrolling: ${JSON.stringify(params)}`) - - let target = new vscode.Range( - new vscode.Position(Math.max(0, params.line - 2), 0), - new vscode.Position(params.line + 2, 0) - ) - - editor.revealRange(target, vscode.TextEditorRevealType.AtTop) - } - private scrollView(editor: vscode.TextEditor) { if (editor.document.uri !== this.currentUri) { return @@ -103,7 +85,7 @@ export class PreviewManager { // More than one range here implies that some regions of code have been folded. // Though I doubt it matters too much for this use case?.. let range = editor.visibleRanges[0] - this.client.scrollView(range.start.line + 1) + this.client.scrollView(editor.document.uri, range.start.line + 1) } private async onDidChangeEditor(editor?: vscode.TextEditor) { @@ -120,6 +102,11 @@ export class PreviewManager { } private async previewEditor(editor: vscode.TextEditor, placement?: vscode.ViewColumn) { + + if (this.changingDocument) { + return + } + if (this.currentUri === editor.document.uri && this.panel) { // There is nothing to do. return @@ -141,12 +128,40 @@ export class PreviewManager { this.currentUri = editor.document.uri } - private showDocument(params: { params: ShowDocumentParams, default: any }) { + private showDocument(req: { params: ShowDocumentParams, default: any }) { + let params = req.params + if (!params.external) { + return this.showInternalDocument(params) + } + if (!this.panel) { return } - this.panel.webview.postMessage({ 'show': params.params.uri }) + this.panel.webview.postMessage({ 'show': params.uri }) + } + + private showInternalDocument(params: ShowDocumentParams) { + this.changingDocument = true + this.currentUri = vscode.Uri.parse(params.uri) + + vscode.window.showTextDocument( + this.currentUri, + { + preserveFocus: true, + // Force document to open in column one, otherwise VSCode may open editor over the + // preview pane. + viewColumn: vscode.ViewColumn.One + } + ).then(editor => { + const range = selectionToRange(params.selection) + if (range) { + editor.revealRange(range, vscode.TextEditorRevealType.AtTop) + } + this.changingDocument = false + }) + + return } private getPanel(placement: vscode.ViewColumn): vscode.WebviewPanel { @@ -342,6 +357,17 @@ export class PreviewManager { } +function selectionToRange(selection?: Range): vscode.Range | undefined { + if (!selection) { + return + } + + return new vscode.Range( + new vscode.Position(selection.start.line, selection.start.character), + new vscode.Position(selection.end.line, selection.end.character), + ) +} + /** * Return the text editor showing the given uri. * @param uri The uri of the document in the editor diff --git a/code/src/node/processTreeView.ts b/code/src/node/processTreeView.ts new file mode 100644 index 000000000..3c6d0d123 --- /dev/null +++ b/code/src/node/processTreeView.ts @@ -0,0 +1,252 @@ +import * as vscode from 'vscode' +import { Notifications, Events } from "../common/constants"; +import { AppCreatedNotification, ClientCreatedNotification, ClientDestroyedNotification, ClientErroredNotification, EsbonioClient, SphinxClientConfig, SphinxInfo } from './client'; + +/** + * Tree View provider that visualises the Sphinx processes currently + * managed by the language server. + */ +export class SphinxProcessProvider implements vscode.TreeDataProvider { + + private sphinxClients: Map = new Map() + + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor(client: EsbonioClient) { + client.addHandler( + Notifications.SPHINX_CLIENT_CREATED, + (params: ClientCreatedNotification) => this.clientCreated(params) + ) + + client.addHandler( + Notifications.SPHINX_APP_CREATED, + (params: AppCreatedNotification) => this.appCreated(params) + ) + + client.addHandler( + Notifications.SPHINX_CLIENT_ERRORED, + (params: ClientErroredNotification) => this.clientErrored(params) + ) + + client.addHandler( + Notifications.SPHINX_CLIENT_DESTROYED, + (params: ClientDestroyedNotification) => this.clientDestroyed(params) + ) + + client.addHandler( + Events.SERVER_STOP, + (_: any) => { this.serverStopped() } + ) + } + + /** + * Return the UI representation of the given element + * + * @param element The tree view element to visualise + * @returns The UI representation of the given element + */ + getTreeItem(element: ProcessTreeNode): vscode.TreeItem { + + switch (element.kind) { + case 'container': + return { label: element.name, collapsibleState: vscode.TreeItemCollapsibleState.Expanded } + + case 'process': + let label = 'Starting...' + let icon: vscode.ThemeIcon | undefined = new vscode.ThemeIcon("sync~spin") + let client = this.sphinxClients.get(element.id)! + let tooltip + + if (client.state === 'running') { + label = `Sphinx ${client.app?.version}` + icon = undefined + + } else if (client.state === 'errored') { + label = client.errorMessage || 'Errored' + icon = new vscode.ThemeIcon("error", new vscode.ThemeColor("list.errorForeground")) + tooltip = new vscode.MarkdownString("```" + (client.errorDetails || '') + "```") + } + + return { + label: label, + iconPath: icon, + tooltip: tooltip, + contextValue: element.kind, + collapsibleState: vscode.TreeItemCollapsibleState.None + } + + case 'property': + return { label: 'Prop', collapsibleState: vscode.TreeItemCollapsibleState.None } + } + + } + + /** + * Return the children of the given element. + * + * When element is `undefined`, return the top level items in the tree. + * + * @param element The element to return children for + * @returns The given element's children + */ + getChildren(element?: ProcessTreeNode): Thenable { + const result: ProcessTreeNode[] = [] + + if (!element) { + for (let process of this.sphinxClients.values()) { + + let cwd = process.config.cwd + let node: ProcssContainerNode = { kind: 'container', name: cwd, path: cwd } + result.push(node) + } + + return Promise.resolve(result) + } + + switch (element.kind) { + case 'container': + for (let [id, process] of this.sphinxClients.entries()) { + if (element.name === process.config.cwd) { + let node: SphinxProcessNode = { kind: 'process', id: id } + result.push(node) + } + } + break + case 'process': + case 'property': + } + + return Promise.resolve(result) + } + + /** + * Called when a new SphinxClient has been created. + * + * @param params Information about the newly created client. + */ + private clientCreated(params: ClientCreatedNotification) { + this.sphinxClients.set(params.id, new SphinxProcess(params.config)) + this._onDidChangeTreeData.fire() + } + + /** + * Called when a new Sphinx application instance has been created. + * + * @param params Information about the newly created app. + */ + private appCreated(params: AppCreatedNotification) { + const client = this.sphinxClients.get(params.id) + if (!client) { return } + + client.setApplication(params.application) + this._onDidChangeTreeData.fire() + } + + /** + * Called when a SphinxClient encounters an error. + * + * @param params Information about the error. + */ + private clientErrored(params: ClientErroredNotification) { + const client = this.sphinxClients.get(params.id) + if (!client) { return } + + client.setError(params.error, params.detail) + this._onDidChangeTreeData.fire() + } + + /** + * Called when a SphinxClient is destroyed. + * + * @param params Information about the event. + */ + private clientDestroyed(params: ClientDestroyedNotification) { + this.sphinxClients.delete(params.id) + this._onDidChangeTreeData.fire() + } + + /** + * Called when the language server exits + */ + private serverStopped() { + this.sphinxClients.clear() + this._onDidChangeTreeData.fire() + } +} + +type ProcessTreeNode = ProcssContainerNode | SphinxProcessNode | ProcessPropertyNode + +/** + * Represents a property of the sphinx process + */ +interface ProcessPropertyNode { + kind: 'property' +} + +/** + * Represents the sphinx process in the tree view + */ +interface SphinxProcessNode { + kind: 'process' + id: string +} + +/** + * Represents a container for the sphinx process + */ +interface ProcssContainerNode { + kind: 'container' + name: string + path: string +} + +class SphinxProcess { + + /** + * Indicates the current state of the process. + */ + public state: 'starting' | 'running' | 'errored' + + /** + * A short message summarising the error + */ + public errorMessage: string | undefined + + /** + * A detailed error message + */ + public errorDetails: string | undefined + + /** + * Application info + */ + public app: SphinxInfo | undefined + + constructor( + public config: SphinxClientConfig, + ) { + this.state = 'starting' + } + + /** + * Called when the underlying process encounters an error + * + * @param error The error message + */ + setError(error: string, detail: string) { + this.state = 'errored' + this.errorMessage = error + this.errorDetails = detail + } + + /** + * Called when the underlying process encounters an error + * + * @param error The error message + */ + setApplication(app: SphinxInfo) { + this.state = 'running' + this.app = app + } +} diff --git a/code/src/node/status.ts b/code/src/node/status.ts deleted file mode 100644 index 2bad5672f..000000000 --- a/code/src/node/status.ts +++ /dev/null @@ -1,189 +0,0 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; - -import { TextDocumentFilter } from 'vscode-languageclient'; -import { Events, Notifications, Server } from '../common/constants'; -import { OutputChannelLogger } from "../common/log"; -import { - AppCreatedNotification, - ClientCreatedNotification, - ClientDestroyedNotification, - ClientErroredNotification, - EsbonioClient -} from './client'; - -interface StatusItemFields { - busy?: boolean - detail?: string, - command?: vscode.Command, - selector?: vscode.DocumentSelector, - severity?: vscode.LanguageStatusSeverity -} - -export class StatusManager { - - private statusItems: Map - - constructor( - private logger: OutputChannelLogger, - private context: vscode.ExtensionContext, - private client: EsbonioClient, - ) { - this.statusItems = new Map() - - client.addHandler( - Events.SERVER_STOP, - (params: any) => { this.serverStop(params) } - ) - - client.addHandler( - Notifications.SPHINX_APP_CREATED, - (params: AppCreatedNotification) => { this.appCreated(params) } - ) - - client.addHandler( - Notifications.SPHINX_CLIENT_CREATED, - (params: ClientCreatedNotification) => { this.clientCreated(params) } - ) - - client.addHandler( - Notifications.SPHINX_CLIENT_ERRORED, - (params: ClientErroredNotification) => { this.clientErrored(params) } - ) - - client.addHandler( - Notifications.SPHINX_CLIENT_DESTROYED, - (params: ClientDestroyedNotification) => { this.clientDestroyed(params) } - ) - } - - private clientCreated(params: ClientCreatedNotification) { - this.logger.debug(`${Notifications.SPHINX_CLIENT_CREATED}: ${JSON.stringify(params, undefined, 2)}`) - let sphinxConfig = params.config - - let config = vscode.workspace.getConfiguration("esbonio.server") - let documentSelector = config.get("documentSelector") - if (!documentSelector || documentSelector.length === 0) { - documentSelector = Server.DEFAULT_SELECTOR - } - - let selector: vscode.DocumentFilter[] = [] - let defaultPattern = path.join(sphinxConfig.cwd, "**", "*") - for (let docSelector of documentSelector) { - selector.push({ - scheme: docSelector.scheme, - language: docSelector.language, - pattern: docSelector.pattern || defaultPattern - }) - } - - this.setStatusItem( - params.id, - "sphinx", - "Sphinx[starting]", - { - selector: selector, - busy: true, - detail: sphinxConfig.buildCommand.join(" "), - severity: vscode.LanguageStatusSeverity.Information - } - ) - this.setStatusItem( - params.id, - "python", - "Python", - { - selector: selector, - detail: sphinxConfig.pythonCommand.join(" "), - command: { title: "Change Interpreter", command: "python.setInterpreter" }, - severity: vscode.LanguageStatusSeverity.Information - } - ) - } - - private clientErrored(params: ClientErroredNotification) { - this.logger.debug(`${Notifications.SPHINX_CLIENT_ERRORED}: ${JSON.stringify(params, undefined, 2)}`) - - this.setStatusItem( - params.id, - "sphinx", - "Sphinx[failed]", - { - busy: false, - detail: params.error, - severity: vscode.LanguageStatusSeverity.Error - } - ) - } - - private clientDestroyed(params: ClientDestroyedNotification) { - this.logger.debug(`${Notifications.SPHINX_CLIENT_DESTROYED}: ${JSON.stringify(params, undefined, 2)}`) - - for (let [key, item] of this.statusItems.entries()) { - if (key.startsWith(params.id)) { - item.dispose() - this.statusItems.delete(key) - } - } - } - - private appCreated(params: AppCreatedNotification) { - this.logger.debug(`${Notifications.SPHINX_APP_CREATED}: ${JSON.stringify(params, undefined, 2)}`) - let sphinx = params.application - - this.setStatusItem( - params.id, - "sphinx", - `Sphinx[${sphinx.builder_name}] v${sphinx.version}`, - { - busy: false, - } - ) - } - - private serverStop(_params: any) { - for (let [key, item] of this.statusItems.entries()) { - item.dispose() - this.statusItems.delete(key) - } - } - - private setStatusItem( - id: string, - name: string, - value: string, - params?: StatusItemFields, - ) { - let key = `${id}-${name.toLocaleLowerCase().replace(' ', '-')}` - let statusItem = this.statusItems.get(key) - - if (!statusItem) { - statusItem = vscode.languages.createLanguageStatusItem(key, { language: "restructuredtext" }) - statusItem.name = name - - this.statusItems.set(key, statusItem) - } - - statusItem.text = value - - if (params && params.busy !== undefined) { - statusItem.busy = params.busy - } - - if (params && params.detail) { - statusItem.detail = params.detail - } - - if (params && params.severity && params.severity >= 0) { - statusItem.severity = params.severity - } - - if (params && params.command) { - statusItem.command = params.command - } - - if (params && params.selector) { - statusItem.selector = params.selector - } - } -} diff --git a/docs/ext/domain.py b/docs/ext/domain.py index 39b4d8ba9..87b597f49 100644 --- a/docs/ext/domain.py +++ b/docs/ext/domain.py @@ -69,6 +69,25 @@ def add_target_and_index( domain.config_values[name] = (self.env.docname, node_id) +class Command(ObjectDescription[str]): + """Description of a ``workspace/executeCommand`` command.""" + + option_spec: OptionSpec = {} + + 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.commands[name] = (self.env.docname, node_id) + + class EsbonioDomain(Domain): """A domain dedicated to documenting the esbonio language server""" @@ -77,24 +96,32 @@ class EsbonioDomain(Domain): object_types: Dict[str, ObjType] = { "config": ObjType("config", "conf"), + "command": ObjType("command", "cmd"), } directives = { "config": ConfigValue, + "command": Command, } roles = { "conf": XRefRole(), + "cmd": XRefRole(), } initial_data = { "config_values": {}, + "commands": {}, } @property def config_values(self) -> dict[str, Tuple[str, str]]: return self.data.setdefault("config_values", {}) + @property + def commands(self) -> dict[str, Tuple[str, str]]: + return self.data.setdefault("commands", {}) + def resolve_xref( self, env: BuildEnvironment, @@ -110,6 +137,9 @@ def resolve_xref( if (entry := self.config_values.get(target, None)) is None: return None + if (entry := self.commands.get(target, None)) is None: + return None + return make_refnode( builder, fromdocname, entry[0], entry[1], [contnode], target ) diff --git a/docs/lsp/extending.rst b/docs/extending/getting-started.rst similarity index 55% rename from docs/lsp/extending.rst rename to docs/extending/getting-started.rst index 362a8d438..d9629f4be 100644 --- a/docs/lsp/extending.rst +++ b/docs/extending/getting-started.rst @@ -1,6 +1,6 @@ .. _lsp-extending: -Extending -========= +Extending Esbonio +================= Coming soon\ :sup:`TM` diff --git a/docs/index.rst b/docs/index.rst index c5ebee9ff..f12f9aa96 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,17 +43,26 @@ The primary goal of Esbonio is to reduce the friction that comes from trying to Documentation on extending the language server .. toctree:: - :glob: :caption: Language Server :hidden: - :maxdepth: 2 lsp/getting-started - lsp/extending lsp/howto lsp/reference changelog +.. toctree:: + :caption: Extending + :hidden: + + Getting Started + +.. toctree:: + :caption: Integrating + :hidden: + + Getting Started + Sphinx Extensions ----------------- diff --git a/docs/integrating/getting-started.rst b/docs/integrating/getting-started.rst new file mode 100644 index 000000000..959c436b0 --- /dev/null +++ b/docs/integrating/getting-started.rst @@ -0,0 +1,50 @@ +Editor Integration +------------------ + +.. toctree:: + :hidden: + + Emacs + Kate + Neovim + +While the :doc:`tutorial ` focuses on using ``esbonio`` from within VSCode. +These guides will help you get ``esbonio`` setup with your editor of choice + +.. admonition:: Don't see your favourite editor? + + Feel free to submit a pull request with steps on how to get started or if you're not + sure on where to start, `open an issue`_ and we'll help you figure it out. + +.. _open an issue: https://github.com/swyddfa/esbonio/issues/new + +.. grid:: 2 2 3 4 + :gutter: 1 + + .. grid-item-card:: Emacs + :link: integrate-emacs + :link-type: ref + :text-align: center + + How to use esbonio within Emacs, using either ``eglot`` or ``lsp-mode`` as your language client. + + .. grid-item-card:: Neovim + :link: integrate-nvim + :link-type: ref + :text-align: center + + Using ``esbonio`` with Neovim's built in language client. + +Additional Features +=================== + +The language server provides additional notifications and commands that are not part of the LSP specification. +This means that they require additional client side integration code to be enabled. + +Theses features are not a requirement to use ``esbonio``, but they do offer some quality of life improvements. + +.. toctree:: + :maxdepth: 1 + + Sphinx Processes + Previews diff --git a/docs/lsp/editors/_installation.rst b/docs/integrating/howto/editors/_installation.rst similarity index 100% rename from docs/lsp/editors/_installation.rst rename to docs/integrating/howto/editors/_installation.rst diff --git a/docs/lsp/editors/kate.rst b/docs/integrating/howto/editors/kate.rst similarity index 89% rename from docs/lsp/editors/kate.rst rename to docs/integrating/howto/editors/kate.rst index c5fb8dc15..7cbba7554 100644 --- a/docs/lsp/editors/kate.rst +++ b/docs/integrating/howto/editors/kate.rst @@ -1,5 +1,7 @@ -Kate -==== +.. _integrate-kate: + +How To Integrate Esbonio with Kate +================================== .. figure:: /images/kate-screenshot.png :align: center @@ -35,7 +37,7 @@ Installation "servers": { "rst": { "command": ["python", "-m", "esbonio"], - "initializationOptions": { + "settings": { "sphinx": { }, "server": { "completion": { @@ -49,7 +51,7 @@ Installation } } - For best results, we recommend you set :confval:`server.completion.preferredInsertBehavior (string)` to ``insert``, see the section on :ref:`lsp-configuration` for details on all the available options. + For best results, we recommend you set :esbonio:conf:`esbonio.server.completion.preferredInsertBehavior` to ``insert``, see the section on :ref:`lsp-configuration` for details on all the available options. For more details on Kate's LSP client see the `project's `__ documentation. diff --git a/docs/lsp/editors/nvim-lspconfig.rst b/docs/integrating/howto/editors/nvim-lspconfig.rst similarity index 100% rename from docs/lsp/editors/nvim-lspconfig.rst rename to docs/integrating/howto/editors/nvim-lspconfig.rst diff --git a/docs/lsp/editors/nvim-lspconfig/.gitignore b/docs/integrating/howto/editors/nvim-lspconfig/.gitignore similarity index 100% rename from docs/lsp/editors/nvim-lspconfig/.gitignore rename to docs/integrating/howto/editors/nvim-lspconfig/.gitignore diff --git a/docs/lsp/editors/nvim-lspconfig/default.nix b/docs/integrating/howto/editors/nvim-lspconfig/default.nix similarity index 100% rename from docs/lsp/editors/nvim-lspconfig/default.nix rename to docs/integrating/howto/editors/nvim-lspconfig/default.nix diff --git a/docs/lsp/editors/nvim-lspconfig/init.vim b/docs/integrating/howto/editors/nvim-lspconfig/init.vim similarity index 100% rename from docs/lsp/editors/nvim-lspconfig/init.vim rename to docs/integrating/howto/editors/nvim-lspconfig/init.vim diff --git a/docs/lsp/editors/nvim.rst b/docs/integrating/howto/editors/nvim.rst similarity index 100% rename from docs/lsp/editors/nvim.rst rename to docs/integrating/howto/editors/nvim.rst diff --git a/docs/lsp/editors/sublime.rst b/docs/integrating/howto/editors/sublime.rst similarity index 100% rename from docs/lsp/editors/sublime.rst rename to docs/integrating/howto/editors/sublime.rst diff --git a/docs/lsp/editors/sublime/LSP.sublime-settings b/docs/integrating/howto/editors/sublime/LSP.sublime-settings similarity index 100% rename from docs/lsp/editors/sublime/LSP.sublime-settings rename to docs/integrating/howto/editors/sublime/LSP.sublime-settings diff --git a/docs/lsp/editors/vim-coc/.gitignore b/docs/integrating/howto/editors/vim-coc/.gitignore similarity index 100% rename from docs/lsp/editors/vim-coc/.gitignore rename to docs/integrating/howto/editors/vim-coc/.gitignore diff --git a/docs/lsp/editors/vim-coc/_configuration.rst b/docs/integrating/howto/editors/vim-coc/_configuration.rst similarity index 100% rename from docs/lsp/editors/vim-coc/_configuration.rst rename to docs/integrating/howto/editors/vim-coc/_configuration.rst diff --git a/docs/lsp/editors/vim-coc/_debugging.rst b/docs/integrating/howto/editors/vim-coc/_debugging.rst similarity index 100% rename from docs/lsp/editors/vim-coc/_debugging.rst rename to docs/integrating/howto/editors/vim-coc/_debugging.rst diff --git a/docs/lsp/editors/vim-coc/_examples.rst b/docs/integrating/howto/editors/vim-coc/_examples.rst similarity index 100% rename from docs/lsp/editors/vim-coc/_examples.rst rename to docs/integrating/howto/editors/vim-coc/_examples.rst diff --git a/docs/lsp/editors/vim-coc/_installation.rst b/docs/integrating/howto/editors/vim-coc/_installation.rst similarity index 100% rename from docs/lsp/editors/vim-coc/_installation.rst rename to docs/integrating/howto/editors/vim-coc/_installation.rst diff --git a/docs/lsp/editors/vim-coc/init.vim b/docs/integrating/howto/editors/vim-coc/init.vim similarity index 100% rename from docs/lsp/editors/vim-coc/init.vim rename to docs/integrating/howto/editors/vim-coc/init.vim diff --git a/docs/lsp/editors/vim-lsp/.gitignore b/docs/integrating/howto/editors/vim-lsp/.gitignore similarity index 100% rename from docs/lsp/editors/vim-lsp/.gitignore rename to docs/integrating/howto/editors/vim-lsp/.gitignore diff --git a/docs/lsp/editors/vim-lsp/_configuration.rst b/docs/integrating/howto/editors/vim-lsp/_configuration.rst similarity index 100% rename from docs/lsp/editors/vim-lsp/_configuration.rst rename to docs/integrating/howto/editors/vim-lsp/_configuration.rst diff --git a/docs/lsp/editors/vim-lsp/_debugging.rst b/docs/integrating/howto/editors/vim-lsp/_debugging.rst similarity index 100% rename from docs/lsp/editors/vim-lsp/_debugging.rst rename to docs/integrating/howto/editors/vim-lsp/_debugging.rst diff --git a/docs/lsp/editors/vim-lsp/_examples.rst b/docs/integrating/howto/editors/vim-lsp/_examples.rst similarity index 100% rename from docs/lsp/editors/vim-lsp/_examples.rst rename to docs/integrating/howto/editors/vim-lsp/_examples.rst diff --git a/docs/lsp/editors/vim-lsp/_installation.rst b/docs/integrating/howto/editors/vim-lsp/_installation.rst similarity index 100% rename from docs/lsp/editors/vim-lsp/_installation.rst rename to docs/integrating/howto/editors/vim-lsp/_installation.rst diff --git a/docs/lsp/editors/vim-lsp/init.vim b/docs/integrating/howto/editors/vim-lsp/init.vim similarity index 100% rename from docs/lsp/editors/vim-lsp/init.vim rename to docs/integrating/howto/editors/vim-lsp/init.vim diff --git a/docs/lsp/editors/vim.rst b/docs/integrating/howto/editors/vim.rst similarity index 100% rename from docs/lsp/editors/vim.rst rename to docs/integrating/howto/editors/vim.rst diff --git a/docs/lsp/editors/vscode.rst b/docs/integrating/howto/editors/vscode.rst similarity index 100% rename from docs/lsp/editors/vscode.rst rename to docs/integrating/howto/editors/vscode.rst diff --git a/docs/lsp/howto/use-esbonio-in-emacs.rst b/docs/integrating/howto/emacs.rst similarity index 94% rename from docs/lsp/howto/use-esbonio-in-emacs.rst rename to docs/integrating/howto/emacs.rst index 49f1f43d7..0c6ab5e59 100644 --- a/docs/lsp/howto/use-esbonio-in-emacs.rst +++ b/docs/integrating/howto/emacs.rst @@ -1,5 +1,7 @@ -How To Use Esbonio in Emacs -=========================== +.. _integrate-emacs: + +How To Integrate Esbonio with Emacs +=================================== There are two main language client implementations available in Emacs diff --git a/docs/lsp/howto/use-esbonio-in-nvim.rst b/docs/integrating/howto/nvim.rst similarity index 92% rename from docs/lsp/howto/use-esbonio-in-nvim.rst rename to docs/integrating/howto/nvim.rst index 02c015807..48749945e 100644 --- a/docs/lsp/howto/use-esbonio-in-nvim.rst +++ b/docs/integrating/howto/nvim.rst @@ -1,5 +1,7 @@ -How To use Esbonio in Neovim -============================ +.. _integrate-nvim: + +How To Integrate Esbonio with Neovim +==================================== This guide covers how to setup ``esbonio`` with Neovim's built-in language client. @@ -56,7 +58,7 @@ For example the ``find_venv`` function below implements the following discovery - If ``nvim`` is launched with a virtual environment active, use it, otherwise - Look for a virtual environment located within the project's git repository -.. literalinclude:: ../editors/nvim-lspconfig/init.vim +.. literalinclude:: ./editors/nvim-lspconfig/init.vim :language: lua :start-at: function find_venv() :end-before: lspconfig @@ -96,9 +98,9 @@ This configuration includes: .. dropdown:: Show Example Config - You can also :download:`download <../editors/nvim-lspconfig/init.vim>` this file + You can also :download:`download <./editors/nvim-lspconfig/init.vim>` this file - .. literalinclude:: ../editors/nvim-lspconfig/init.vim + .. literalinclude:: ./editors/nvim-lspconfig/init.vim :language: vim diff --git a/docs/lsp/reference/previews.rst b/docs/integrating/reference/previews.rst similarity index 80% rename from docs/lsp/reference/previews.rst rename to docs/integrating/reference/previews.rst index 83d8b0fd9..9c713f2e5 100644 --- a/docs/lsp/reference/previews.rst +++ b/docs/integrating/reference/previews.rst @@ -1,5 +1,5 @@ -Preview Implementation -====================== +Previews +======== This page gives an overview of how the preview feature in ``esbonio`` is implemented. @@ -31,3 +31,18 @@ At a high level the typical interaction between the components may look somethin #. Once the page loads in the webview, the WebSocket client embedded in the page connects to the WebSocket server running in the Language Server. Using the Language Server as an intermediary, it's now possible for the editor and webview to communicate with each other. #. When the user scrolls the editor, the editor informs the webview where it should scroll to (and vice versa). + +Commands +-------- + +The server offers the following commands for controlling the underlying Sphinx processes. +They are invoked using an :lsp:`workspace/executeCommand` request + +.. esbonio:command:: esbonio.server.previewFile + + Preview the output for the given file. + This command accepts an object of the following form + + .. code-block:: json + + {"uri": "a0a5a856-d4ec-4c45-8461-78748ddbd06f"} diff --git a/docs/integrating/reference/sphinx-processes.rst b/docs/integrating/reference/sphinx-processes.rst new file mode 100644 index 000000000..ce4dd0237 --- /dev/null +++ b/docs/integrating/reference/sphinx-processes.rst @@ -0,0 +1,40 @@ +Sphinx Process Management +========================= + +The language server provides additional notifications and commands that the client can use to provide greater insight and control over the Sphinx sub-processes mananged by ``esbonio`` + +Life-Cycle Notifications +------------------------ + +The following notifications will be emitted following during the life-cycle of a Sphinx sub-process + +.. currentmodule:: esbonio.server.features.sphinx_manager.manager + +.. autoclass:: ClientCreatedNotification + :members: + +.. autoclass:: AppCreatedNotification + :members: + +.. autoclass:: ClientErroredNotification + :members: + +.. autoclass:: ClientDestroyedNotification + :members: + +Commands +-------- + +The server offers the following commands for controlling the underlying Sphinx processes. +They are invoked using an :lsp:`workspace/executeCommand` request + +.. esbonio:command:: esbonio.sphinx.restart + + Restart the given Sphinx client(s). + This command accepts a list of objects of the following form + + .. code-block:: json + + {"id": "a0a5a856-d4ec-4c45-8461-78748ddbd06f"} + + Where each ``id`` corresponds to a Sphinx Client. diff --git a/docs/lsp/howto.rst b/docs/lsp/howto.rst index 0f16916b8..8038bfc12 100644 --- a/docs/lsp/howto.rst +++ b/docs/lsp/howto.rst @@ -9,40 +9,3 @@ This section contains a number of guides to help you get the most out of Esbonio :hidden: Migrate to v1 - -Editor Integration ------------------- - -.. toctree:: - :hidden: - :maxdepth: 1 - - Use Esbonio in Emacs - Use Esbonio in Neovim - -While the :doc:`tutorial ` focuses on using ``esbonio`` from within VSCode. -These guides will help you get ``esbonio`` setup with your editor of choice - -.. admonition:: Don't see your favourite editor? - - Feel free to submit a pull request with steps on how to get started or if you're not - sure on where to start, `open an issue`_ and we'll help you figure it out. - -.. _open an issue: https://github.com/swyddfa/esbonio/issues/new - -.. grid:: 2 2 3 4 - :gutter: 1 - - .. grid-item-card:: Emacs - :link: howto/use-esbonio-in-emacs - :link-type: doc - :text-align: center - - How to use esbonio within Emacs, using either ``eglot`` or ``lsp-mode`` as your language client. - - .. grid-item-card:: Neovim - :link: howto/use-esbonio-in-nvim - :link-type: doc - :text-align: center - - Using ``esbonio`` with Neovim's built in language client. diff --git a/docs/lsp/reference/notifications.rst b/docs/lsp/reference/notifications.rst deleted file mode 100644 index 3af90708f..000000000 --- a/docs/lsp/reference/notifications.rst +++ /dev/null @@ -1,18 +0,0 @@ -Notifications -============= - -In addition to the language server protocol, Esbonio will emit the following notifications - -.. currentmodule:: esbonio.server.features.sphinx_manager.manager - -.. autoclass:: ClientCreatedNotification - :members: - -.. autoclass:: AppCreatedNotification - :members: - -.. autoclass:: ClientErroredNotification - :members: - -.. autoclass:: ClientDestroyedNotification - :members: diff --git a/lib/esbonio/changes/464.feature.md b/lib/esbonio/changes/464.feature.md new file mode 100644 index 000000000..aeceb9e8e --- /dev/null +++ b/lib/esbonio/changes/464.feature.md @@ -0,0 +1 @@ +The language server now generates completions for `:external:` roles and their corresponding targets diff --git a/lib/esbonio/changes/711.fix.md b/lib/esbonio/changes/711.fix.md new file mode 100644 index 000000000..59b19545b --- /dev/null +++ b/lib/esbonio/changes/711.fix.md @@ -0,0 +1 @@ +`esbonio.sphinx.buildCommand` settings provided in a `pyproject.toml` file are now resolved relative to the file's location diff --git a/lib/esbonio/changes/784.enhancement.md b/lib/esbonio/changes/784.enhancement.md new file mode 100644 index 000000000..4f1547896 --- /dev/null +++ b/lib/esbonio/changes/784.enhancement.md @@ -0,0 +1 @@ +Synchronised scrolling now works with files that have been `.. included::`, as well as autodoc docstrings diff --git a/lib/esbonio/changes/786.enhancement.md b/lib/esbonio/changes/786.enhancement.md new file mode 100644 index 000000000..15d0af656 --- /dev/null +++ b/lib/esbonio/changes/786.enhancement.md @@ -0,0 +1 @@ +The resolution of sync scrolling has been improved with the webview also better handling the case where the requested line does not exactly match a known source location diff --git a/lib/esbonio/changes/843.fix.md b/lib/esbonio/changes/843.fix.md new file mode 100644 index 000000000..ca2e6f324 --- /dev/null +++ b/lib/esbonio/changes/843.fix.md @@ -0,0 +1 @@ +The sphinx agent should no longer crash when encountering unexpected config values diff --git a/lib/esbonio/changes/854.feature.md b/lib/esbonio/changes/854.feature.md new file mode 100644 index 000000000..90b6b0c37 --- /dev/null +++ b/lib/esbonio/changes/854.feature.md @@ -0,0 +1 @@ +Add a `esbonio.sphinx.restart` command which, as the name suggests, allows a client to restart one or more Sphinx processes managed by the server diff --git a/lib/esbonio/esbonio/server/__init__.py b/lib/esbonio/esbonio/server/__init__.py index 4b7f4b238..d57f38713 100644 --- a/lib/esbonio/esbonio/server/__init__.py +++ b/lib/esbonio/esbonio/server/__init__.py @@ -1,6 +1,7 @@ from esbonio.sphinx_agent.types import Uri from ._configuration import ConfigChangeEvent +from ._configuration import ConfigurationContext from .events import EventSource from .feature import CompletionConfig from .feature import CompletionContext @@ -14,6 +15,7 @@ __all__ = ( "__version__", "ConfigChangeEvent", + "ConfigurationContext", "CompletionConfig", "CompletionContext", "CompletionTrigger", diff --git a/lib/esbonio/esbonio/server/_configuration.py b/lib/esbonio/esbonio/server/_configuration.py index fb6ed333a..fad620a15 100644 --- a/lib/esbonio/esbonio/server/_configuration.py +++ b/lib/esbonio/esbonio/server/_configuration.py @@ -1,12 +1,10 @@ from __future__ import annotations -import asyncio import inspect import json import pathlib -import traceback +import re import typing -from functools import partial from typing import Generic from typing import TypeVar @@ -55,11 +53,8 @@ class Subscription(Generic[T]): callback: ConfigurationCallback """The subscription's callback.""" - workspace_scope: str - """The corresponding workspace scope for the subscription.""" - - file_scope: str - """The corresponding file scope for the subscription.""" + context: ConfigurationContext + """The context for this subscription.""" @attrs.define @@ -76,6 +71,67 @@ class ConfigChangeEvent(Generic[T]): """The previous configuration value, (if any).""" +VARIABLE = re.compile(r"\$\{(\w+)\}") + + +@attrs.define(frozen=True) +class ConfigurationContext: + """The context in which configuration variables are evaluated.""" + + file_scope: str + """The uri of the file scope of this context.""" + + workspace_scope: str + """The uri of the workspace scope of this context.""" + + @property + def scope(self) -> str: + """The effective scope of the config context.""" + return max([self.file_scope, self.workspace_scope], key=len) + + @property + def scope_path(self) -> Optional[str]: + """The scope uri as a path.""" + uri = Uri.parse(self.scope) + return uri.path + + @property + def scope_fs_path(self) -> Optional[str]: + """The scope uri as an fs path.""" + uri = Uri.parse(self.scope) + return uri.fs_path + + def expand(self, config: attrs.AttrsInstance) -> attrs.AttrsInstance: + """Expand any configuration variables in the given config value.""" + for name in attrs.fields_dict(type(config)): + value = getattr(config, name) + + # For now, we only support variables that are a string. + if not isinstance(value, str): + continue + + if (match := VARIABLE.match(value)) is None: + continue + + setattr(config, name, self.expand_value(match.group(1))) + + return config + + def expand_value(self, variable: str) -> Any: + """Return the value for the given variable name.""" + + if variable == "scope": + return self.scope + + if variable == "scopePath": + return self.scope_path + + if variable == "scopeFsPath": + return self.scope_fs_path + + raise ValueError(f"Undefined variable: {variable!r}") + + class Configuration: """Manages the configuration values for the server. @@ -114,9 +170,6 @@ def __init__(self, server: EsbonioLanguageServer): self._subscriptions: Dict[Subscription, Any] = {} """Subscriptions and their last known value""" - self._tasks: Set[asyncio.Task] = set() - """Holds tasks that are currently executing an async config handler.""" - @property def initialization_options(self): return self._initialization_options @@ -172,9 +225,11 @@ def subscribe( """ file_scope = self._uri_to_file_scope(scope) workspace_scope = self._uri_to_workspace_scope(scope) - subscription = Subscription( - section, spec, callback, workspace_scope, file_scope + + context = ConfigurationContext( + file_scope=file_scope, workspace_scope=workspace_scope ) + subscription = Subscription(section, spec, callback, context) if subscription in self._subscriptions: self.logger.debug("Ignoring duplicate subscription: %s", subscription) @@ -192,8 +247,7 @@ def _notify_subscriptions(self, *args): value = self._get_config( subscription.section, subscription.spec, - subscription.workspace_scope, - subscription.file_scope, + subscription.context, ) # No need to notify if nothing has changed @@ -204,9 +258,7 @@ def _notify_subscriptions(self, *args): self._subscriptions[subscription] = value change_event = ConfigChangeEvent( - scope=max( - [subscription.file_scope, subscription.workspace_scope], key=len - ), + scope=subscription.context.scope, value=value, previous=previous_value, ) @@ -215,8 +267,7 @@ def _notify_subscriptions(self, *args): try: ret = subscription.callback(change_event) if inspect.iscoroutine(ret): - task = asyncio.create_task(ret) - task.add_done_callback(partial(self._finish_task, subscription)) + self.server.run_task(ret) except Exception: self.logger.error( @@ -225,17 +276,6 @@ def _notify_subscriptions(self, *args): exc_info=True, ) - def _finish_task(self, subscription: Subscription, task: asyncio.Task[None]): - """Cleanup a finished task.""" - self._tasks.discard(task) - - if (exc := task.exception()) is not None: - self.logger.error( - "Error in async configuration handler '%s'\n%s", - subscription.callback, - "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)), - ) - def get(self, section: str, spec: Type[T], scope: Optional[Uri] = None) -> T: """Get the requested configuration section. @@ -255,10 +295,12 @@ def get(self, section: str, spec: Type[T], scope: Optional[Uri] = None) -> T: T The requested configuration section parsed as an instance of ``T``. """ - file_scope = self._uri_to_file_scope(scope) - workspace_scope = self._uri_to_workspace_scope(scope) + context = ConfigurationContext( + file_scope=self._uri_to_file_scope(scope), + workspace_scope=self._uri_to_workspace_scope(scope), + ) - return self._get_config(section, spec, workspace_scope, file_scope) + return self._get_config(section, spec, context) def scope_for(self, uri: Uri) -> str: """Return the configuration scope that corresponds to the given uri. @@ -273,24 +315,27 @@ def scope_for(self, uri: Uri) -> str: str The scope corresponding with the given uri """ + context = ConfigurationContext( + file_scope=self._uri_to_file_scope(uri), + workspace_scope=self._uri_to_workspace_scope(uri), + ) - file_scope = self._uri_to_file_scope(uri) - workspace_scope = self._uri_to_workspace_scope(uri) - - return max([file_scope, workspace_scope], key=len) + return context.scope def _get_config( - self, section: str, spec: Type[T], workspace_scope: str, file_scope: str + self, + section: str, + spec: Type[T], + context: ConfigurationContext, ) -> T: """Get the requested configuration section.""" - self.logger.debug("File scope: '%s'", file_scope) - self.logger.debug("Workspace scope: '%s'", workspace_scope) + self.logger.debug("%s", context) # To keep things simple, this method assumes that all available config is already # cached locally. Populating the cache is handled elsewhere. - file_config = self._file_config.get(file_scope, {}) - workspace_config = self._workspace_config.get(workspace_scope, {}) + file_config = self._file_config.get(context.file_scope, {}) + workspace_config = self._workspace_config.get(context.workspace_scope, {}) # Combine and resolve all the config sources - order matters! config = _merge_configs( @@ -303,10 +348,11 @@ def _get_config( for name in section.split("."): config_section = config_section.get(name, {}) - self.logger.debug("Resolved config: %s", json.dumps(config_section, indent=2)) + self.logger.debug("%s: %s", section, json.dumps(config_section, indent=2)) try: value = self.converter.structure(config_section, spec) + value = context.expand(value) self.logger.debug("%s", value) return value @@ -342,7 +388,7 @@ def _discover_config_files(self) -> List[pathlib.Path]: if (folder_path := Uri.parse(uri).fs_path) is None: continue - self.logger.debug("Scanning workspace folder: '%s'", folder_path) + self.logger.debug("Looking for pyproject.toml files in: '%s'", folder_path) for p in pathlib.Path(folder_path).glob("**/pyproject.toml"): self.logger.debug("Found '%s'", p) paths.append(p) diff --git a/lib/esbonio/esbonio/server/features/preview_manager/__init__.py b/lib/esbonio/esbonio/server/features/preview_manager/__init__.py index 5436926f9..723e9bbb9 100644 --- a/lib/esbonio/esbonio/server/features/preview_manager/__init__.py +++ b/lib/esbonio/esbonio/server/features/preview_manager/__init__.py @@ -137,13 +137,13 @@ async def on_build(self, client: SphinxClient, result): self.logger.debug("Refreshing preview") self.webview.reload() - async def scroll_view(self, line: int): + async def scroll_view(self, uri: str, line: int): """Scroll the webview to the given line number.""" if self.webview is None: return - self.webview.scroll(line) + self.webview.scroll(uri, line) async def preview_file(self, params, retry=True): if self.preview is None: @@ -224,7 +224,7 @@ def esbonio_setup( @esbonio.feature("view/scroll") async def on_scroll(ls: server.EsbonioLanguageServer, params): - await manager.scroll_view(params.line) + await manager.scroll_view(params.uri, params.line) @esbonio.command("esbonio.server.previewFile") async def preview_file(ls: server.EsbonioLanguageServer, *args): diff --git a/lib/esbonio/esbonio/server/features/preview_manager/webview.py b/lib/esbonio/esbonio/server/features/preview_manager/webview.py index e4e339c6c..78de93328 100644 --- a/lib/esbonio/esbonio/server/features/preview_manager/webview.py +++ b/lib/esbonio/esbonio/server/features/preview_manager/webview.py @@ -6,6 +6,7 @@ import socket import typing +from lsprotocol import types from pygls.protocol import JsonRPCProtocol from pygls.protocol import default_converter from pygls.server import Server @@ -52,6 +53,9 @@ def __init__(self, logger: logging.Logger, config: PreviewConfig, *args, **kwarg self._view_in_control: Optional[asyncio.Task] = None """If set, the view is in control and the editor should not emit scroll events""" + self._current_uri: Optional[str] = None + """If set, indicates the current uri the editor and view are scrolling.""" + def __await__(self): """Makes the server await-able""" if self._startup_task is None: @@ -80,7 +84,7 @@ def reload(self): if self.connected: self.lsp.notify("view/reload", {}) - def scroll(self, line: int): + def scroll(self, uri: str, line: int): """Called by the editor to scroll the current webview.""" if not self.connected or self._view_in_control: return @@ -89,8 +93,9 @@ def scroll(self, line: int): if self._editor_in_control: self._editor_in_control.cancel() + self._current_uri = uri self._editor_in_control = asyncio.create_task(self.cooldown("editor")) - self.lsp.notify("view/scroll", {"line": line}) + self.lsp.notify("view/scroll", {"uri": uri, "line": line}) async def cooldown(self, name: str): """Create a cooldown.""" @@ -164,6 +169,16 @@ def on_scroll(ls: WebviewServer, params): server._view_in_control.cancel() server._view_in_control = asyncio.create_task(server.cooldown("view")) - esbonio.lsp.notify("editor/scroll", dict(line=params.line)) + + esbonio.lsp.show_document( + types.ShowDocumentParams( + uri=params.uri, + external=False, + selection=types.Range( + start=types.Position(line=params.line - 1, character=0), + end=types.Position(line=params.line, character=0), + ), + ) + ) return server diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/__init__.py b/lib/esbonio/esbonio/server/features/sphinx_manager/__init__.py index da73648ad..258181719 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/__init__.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/__init__.py @@ -20,3 +20,13 @@ def esbonio_setup(server: EsbonioLanguageServer, project_manager: ProjectManager): manager = SphinxManager(make_subprocess_sphinx_client, project_manager, server) server.add_feature(manager) + + @server.command("esbonio.sphinx.restart") + async def restart_client(ls: EsbonioLanguageServer, params, *args): + ls.logger.debug("esbonio.sphinx.restart: %s", params) + + for item in params: + if item is None: + continue + + await manager.restart_client(item["id"]) diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/client.py b/lib/esbonio/esbonio/server/features/sphinx_manager/client.py index 78b5d0892..f83e4c8b0 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/client.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/client.py @@ -22,6 +22,9 @@ class ClientState(enum.Enum): Starting = enum.auto() """The client is starting.""" + Restarting = enum.auto() + """The client is restarting.""" + Running = enum.auto() """The client is running normally.""" @@ -81,6 +84,10 @@ async def start(self) -> SphinxClient: """Start the client.""" ... + async def restart(self) -> SphinxClient: + """Restart the client.""" + ... + async def build( self, *, 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 a8305e4b1..d642c68d1 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/client_subprocess.py @@ -187,11 +187,21 @@ async def start_io(self, cmd: str, *args, **kwargs): if self._server and self._server.stderr: self._stderr_forwarder = asyncio.create_task(forward_stderr(self._server)) + async def restart(self) -> SphinxClient: + """Restart the client.""" + await self.stop() + + # We need to reset the client's stop event. + self._stop_event.clear() + + self._set_state(ClientState.Restarting) + return await self.start() + async def start(self) -> SphinxClient: """Start the client.""" # Only try starting once. - if self.state is not None: + if self.state not in {None, ClientState.Restarting}: return self try: @@ -205,7 +215,6 @@ async def start(self) -> SphinxClient: params = types.CreateApplicationParams( command=self.config.build_command, ) - self.sphinx_info = await self.protocol.send_request_async( "sphinx/createApp", params ) diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/config.py b/lib/esbonio/esbonio/server/features/sphinx_manager/config.py index 855f53d38..53e0c48f2 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/config.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/config.py @@ -54,7 +54,7 @@ class SphinxConfig: env_passthrough: List[str] = attrs.field(factory=list) """List of environment variables to pass through to the Sphinx subprocess""" - cwd: str = attrs.field(default="") + cwd: str = attrs.field(default="${scopeFsPath}") """The working directory to use.""" python_path: List[pathlib.Path] = attrs.field(factory=list) @@ -133,8 +133,7 @@ def _resolve_cwd( The working directory to launch the sphinx agent in. If ``None``, the working directory could not be determined. """ - - if self.cwd: + if self.cwd and self.cwd != "${scopeFsPath}": return self.cwd candidates = [Uri.parse(f) for f in workspace.folders.keys()] diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/manager.py b/lib/esbonio/esbonio/server/features/sphinx_manager/manager.py index 61203e8ce..39fa9ff02 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/manager.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/manager.py @@ -164,17 +164,22 @@ async def trigger_build_after(self, uri: Uri, app_id: str, delay: float): await asyncio.sleep(delay) self._pending_builds.pop(app_id) - self.logger.debug("Triggering build") await self.trigger_build(uri) async def trigger_build(self, uri: Uri): """Trigger a build for the relevant Sphinx application for the given uri.""" + self.logger.debug("Triggering build") client = await self.get_client(uri) - if client is None or client.state != ClientState.Running: + if client is None: + return + + if client.state != ClientState.Running: + self.logger.debug("Skipping build, state is: %s", client.state) return if (project := self.project_manager.get_project(uri)) is None: + self.logger.debug("Skipping build, project is None") return # Pass through any unsaved content to the Sphinx agent. @@ -202,6 +207,25 @@ async def trigger_build(self, uri: Uri): # Notify listeners. self._events.trigger("build", client, result) + async def restart_client(self, client_id: str): + """Restart the client with the given id""" + for client in self.clients.values(): + if client is None: + continue + + if client.id != client_id: + continue + + try: + await client.restart() + except Exception: + self.logger.exception("Unable to restart sphinx client") + + break + + else: + self.logger.error(f"No client with id {client_id!r} available to restart") + async def get_client(self, uri: Uri) -> Optional[SphinxClient]: """Given a uri, return the relevant sphinx client instance for it.""" diff --git a/lib/esbonio/esbonio/server/features/sphinx_support/roles.py b/lib/esbonio/esbonio/server/features/sphinx_support/roles.py index 582b17d92..541fe8b27 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_support/roles.py +++ b/lib/esbonio/esbonio/server/features/sphinx_support/roles.py @@ -42,36 +42,63 @@ async def suggest_targets( # type: ignore[override] context: server.CompletionContext, *, obj_types: List[str], + projects: Optional[List[str]], ) -> Optional[List[lsp.CompletionItem]]: - self.logger.debug("Suggesting targets for types: %s", obj_types) + # TODO: Handle .. currentmodule if (project := self.manager.get_project(context.uri)) is None: return None + items = [] 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)) + query, parameters = self._prepare_target_query(projects, obj_types) + cursor = await db.execute(query, parameters) + for name, display, type_ in await cursor.fetchall(): kind = TARGET_KINDS.get(type_, lsp.CompletionItemKind.Reference) items.append( lsp.CompletionItem( label=name, - detail=display, + detail=None if display == "-" else display, kind=kind, ), ) return items + def _prepare_target_query( + self, projects: Optional[List[str]], obj_types: List[str] + ): + """Prepare the query to use when looking up targets.""" + + select = "SELECT name, display, objtype FROM objects" + where = [] + parameters = [] + + if projects is None: + self.logger.debug( + "Suggesting targets from the local project for types: %s", obj_types + ) + where.append("project IS NULL") + + else: + self.logger.debug( + "Suggesting targets from projects %s for types: %s", projects, obj_types + ) + + placeholders = ", ".join("?" for _ in range(len(projects))) + where.append(f"project IN ({placeholders})") + parameters.extend(projects) + + placeholders = ", ".join("?" for _ in range(len(obj_types))) + where.append(f"printf('%s:%s', domain, objtype) IN ({placeholders})") + parameters.extend(obj_types) + + query = " ".join([select, "WHERE", " AND ".join(where)]) + + return query, tuple(parameters) + class SphinxRoles(roles.RoleProvider): """Support for roles in a sphinx project.""" diff --git a/lib/esbonio/esbonio/sphinx_agent/app.py b/lib/esbonio/esbonio/sphinx_agent/app.py index 2b3e65bb7..6cbff3b63 100644 --- a/lib/esbonio/esbonio/sphinx_agent/app.py +++ b/lib/esbonio/esbonio/sphinx_agent/app.py @@ -16,18 +16,14 @@ 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) +logger = sphinx_logger.getChild("esbonio") sphinx_log_setup = sphinx_logging_module.setup @@ -119,24 +115,3 @@ def __init__(self, *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 80be9e26f..db86c8691 100644 --- a/lib/esbonio/esbonio/sphinx_agent/database.py +++ b/lib/esbonio/esbonio/sphinx_agent/database.py @@ -93,8 +93,11 @@ def clear_table(self, table: Table, **kwargs): parameters: List[Any] = [] for param, value in kwargs.items(): - where.append(f"{param} = ?") - parameters.append(value) + if value is None: + where.append(f"{param} is null") + else: + where.append(f"{param} = ?") + parameters.append(value) if where: conditions = " AND ".join(where) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py b/lib/esbonio/esbonio/sphinx_agent/handlers/__init__.py index b669b4a64..613cad2fe 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 pathlib import sys import traceback import typing @@ -18,16 +17,15 @@ from .. import types from ..app import Sphinx from ..config import SphinxConfig -from ..transforms import LineNumberTransform from ..types import Uri from ..util import send_error from ..util import send_message -STATIC_DIR = (pathlib.Path(__file__).parent.parent / "static").resolve() sphinx_logger = logging.getLogger(SPHINX_LOG_NAMESPACE) # Inject our own 'core' extensions into Sphinx sphinx.application.builtin_extensions += ( + f"{__name__}.webview", f"{__name__}.files", f"{__name__}.diagnostics", f"{__name__}.symbols", @@ -118,13 +116,10 @@ def create_sphinx_app(self, request: types.CreateApplicationRequest): self.app = Sphinx(**sphinx_args) # Connect event handlers. - self.app.connect("env-before-read-docs", self._cb_env_before_read_docs) - self.app.connect("source-read", self._cb_source_read, priority=0) - # TODO: Sphinx 7.x has introduced a `include-read` event # See: https://github.com/sphinx-doc/sphinx/pull/11657 - - _enable_sync_scrolling(self.app) + self.app.connect("env-before-read-docs", self._cb_env_before_read_docs) + self.app.connect("source-read", self._cb_source_read, priority=0) response = types.CreateApplicationResponse( id=request.id, @@ -191,17 +186,3 @@ def build_sphinx_app(self, request: types.BuildRequest): def notify_exit(self, request: types.ExitNotification): """Sent from the client to signal that the agent should exit.""" sys.exit(0) - - -def _enable_sync_scrolling(app: Sphinx): - """Given a Sphinx application, configure it so that we can support syncronised - scrolling.""" - - # Inline the JS code we need to enable sync scrolling. - # - # 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()) - - app.add_transform(LineNumberTransform) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/domains.py b/lib/esbonio/esbonio/sphinx_agent/handlers/domains.py index 661f693e7..07d4b58ef 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/domains.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/domains.py @@ -11,9 +11,26 @@ if typing.TYPE_CHECKING: from typing import Dict + from typing import List from typing import Optional + from typing import Set from typing import Tuple + from sphinx.domains import Domain + from sphinx.util.typing import Inventory + + +PROJECTS_TABLE = Database.Table( + "intersphinx_projects", + [ + Database.Column(name="id", dtype="TEXT"), + Database.Column(name="name", dtype="TEXT"), + Database.Column(name="version", dtype="TEXT"), + Database.Column(name="uri", dtype="TEXT"), + ], +) + + OBJECTS_TABLE = Database.Table( "objects", [ @@ -22,6 +39,7 @@ Database.Column(name="domain", dtype="TEXT"), Database.Column(name="objtype", dtype="TEXT"), Database.Column(name="docname", dtype="TEXT"), + Database.Column(name="project", dtype="TEXT"), Database.Column(name="description", dtype="TEXT"), Database.Column(name="location", dtype="JSON"), ], @@ -36,13 +54,18 @@ def __init__(self, app: Sphinx): Tuple[str, str, str, str], Tuple[Optional[str], Optional[str]] ] = {} - app.connect("builder-inited", self.init_db) + # Needs to run late, but before the handler in ./roles.py + app.connect("builder-inited", self.init_db, priority=998) 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) + projects = index_intersphinx_projects(app) + project_names = [p[0] for p in projects] + + for domain in app.env.domains.values(): + index_domain(app, domain, project_names) def commit(self, app, exc): """Commit changes to the database. @@ -54,7 +77,7 @@ def commit(self, app, exc): 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) + app.esbonio.db.clear_table(OBJECTS_TABLE, project=None) rows = [] for name, domain in app.env.domains.items(): @@ -62,8 +85,12 @@ def commit(self, app, exc): desc, location = self._info.get( (objname, name, objtype, docname), (None, None) ) + + if objname == (display := str(dispname)): + display = "-" + rows.append( - (objname, str(dispname), name, objtype, docname, desc, location) + (objname, display, name, objtype, docname, None, desc, location) ) app.esbonio.db.insert_values(OBJECTS_TABLE, rows) @@ -112,5 +139,117 @@ def object_defined( self._info[key] = (description, location) +def index_domain(app: Sphinx, domain: Domain, projects: Optional[List[str]]): + """Index the roles in the given domain. + + Parameters + ---------- + app + The application instance + + domain + The domain to index + + projects + The list of known intersphinx projects + """ + 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(): + if (item_types := target_types.get(name)) is None: + app.esbonio.add_role(f"{domain.name}:{name}", role, []) + continue + + # Add an entry for the local project. + provider = app.esbonio.create_role_target_provider( + "objects", obj_types=list(item_types), projects=None + ) + app.esbonio.add_role(f"{domain.name}:{name}", role, [provider]) + + if projects is None or len(projects) == 0: + continue + + # Add an entry referencing all external projects + provider = app.esbonio.create_role_target_provider( + "objects", obj_types=list(item_types), projects=projects + ) + app.esbonio.add_role(f"external:{domain.name}:{name}", role, [provider]) + + # Add an entry dedicated to each external project + for project in projects: + provider = app.esbonio.create_role_target_provider( + "objects", obj_types=list(item_types), projects=[project] + ) + app.esbonio.add_role( + f"external+{project}:{domain.name}:{name}", role, [provider] + ) + + +def index_intersphinx_projects(app: Sphinx) -> List[Tuple[str, str, str, str]]: + """Index all the projects known to intersphinx. + + Parameters + ---------- + app + The application instance + + Returns + ------- + List[Tuple[str, str, str, str]] + The list of discovered projects + """ + app.esbonio.db.ensure_table(OBJECTS_TABLE) + app.esbonio.db.ensure_table(PROJECTS_TABLE) + app.esbonio.db.clear_table(PROJECTS_TABLE) + + projects: List[Tuple[str, str, str, str]] = [] + objects = [] + + mapping = getattr(app.config, "intersphinx_mapping", {}) + inventory = getattr(app.env, "intersphinx_named_inventory", {}) + + for id_, (_, (uri, _)) in mapping.items(): + if (project := inventory.get(id_, None)) is None: + continue + + app.esbonio.db.clear_table(OBJECTS_TABLE, project=id_) + + # We just need an entry to be able to extract the project name and version + (name, version, _, _) = next(iter(next(iter(project.values())).values())) + + projects.append((id_, name, version, uri)) + objects.extend(index_intersphinx_objects(id_, uri, project)) + + app.esbonio.db.insert_values(PROJECTS_TABLE, projects) + app.esbonio.db.insert_values(OBJECTS_TABLE, objects) + + return projects + + +def index_intersphinx_objects(project_name: str, uri: str, project: Inventory): + """Index all the objects in the given project.""" + + objects = [] + + for objtype, items in project.items(): + domain = None + if ":" in objtype: + domain, *parts = objtype.split(":") + objtype = ":".join(parts) + + for objname, (_, _, item_uri, display) in items.items(): + docname = item_uri.replace(uri, "") + objects.append( + (objname, display, domain, objtype, docname, project_name, None, None) + ) + + return objects + + def setup(app: Sphinx): DomainObjects(app) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/files.py b/lib/esbonio/esbonio/sphinx_agent/handlers/files.py index cccd470a1..eeab09093 100644 --- a/lib/esbonio/esbonio/sphinx_agent/handlers/files.py +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/files.py @@ -4,10 +4,12 @@ from ..app import Database from ..app import Sphinx +from ..app import logger from ..types import Uri from ..util import as_json if typing.TYPE_CHECKING: + from typing import Any from typing import List from typing import Optional from typing import Tuple @@ -45,6 +47,16 @@ def init_db(app: Sphinx, config: Config): app.esbonio.db.ensure_table(CONFIG_TABLE) +def value_to_db(name: str, item: Any) -> Tuple[str, str, Any]: + """Convert a single value to its DB representation""" + + try: + (value, scope, _) = item + return (name, scope, as_json(value)) + except Exception: + return (name, "", as_json(item)) + + def dump_config(app: Sphinx, *args): """Dump the user's config into the db so that the parent language server can inspect it.""" @@ -61,20 +73,18 @@ def dump_config(app: Sphinx, *args): continue try: - (value, scope, _) = item - values.append((name, scope, as_json(value))) - except Exception: - values.append((name, "", as_json(item))) + values.append(value_to_db(name, item)) + except Exception as exc: + logger.debug(f"Unable to dump config value: {name!r}: {exc}") for name, item in config.items(): if name in IGNORED_CONFIG_NAMES: continue try: - (value, scope, _) = item - values.append((name, scope, as_json(value))) - except Exception: - values.append((name, "", as_json(item))) + values.append(value_to_db(name, item)) + except Exception as exc: + logger.debug(f"Unable to dump config value: {name!r}: {exc}") app.esbonio.db.insert_values(CONFIG_TABLE, values) diff --git a/lib/esbonio/esbonio/sphinx_agent/handlers/webview.py b/lib/esbonio/esbonio/sphinx_agent/handlers/webview.py new file mode 100644 index 000000000..28092141a --- /dev/null +++ b/lib/esbonio/esbonio/sphinx_agent/handlers/webview.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import pathlib +import typing + +from docutils import nodes +from docutils.transforms import Transform +from sphinx import addnodes +from sphinx import version_info + +from ..log import source_to_uri_and_linum + +if typing.TYPE_CHECKING: + from typing import Dict + from typing import Tuple + + from sphinx.application import Sphinx + + +STATIC_DIR = (pathlib.Path(__file__).parent.parent / "static").resolve() + + +def has_source(node): + if isinstance(node, nodes.Text): + return False + + # For some reason, including `toctreenode` causes Sphinx 5.x and 6.x to crash with a + # cryptic error. + # + # AssertionError: Losing "classes" attribute + # + # Caused by this line: + # https://github.com/sphinx-doc/sphinx/blob/ec993dda3690f260345133c47a4a0f6ef0b18493/sphinx/environment/__init__.py#L630 + if isinstance(node, addnodes.toctree) and version_info[0] < 7: + return False + + return (node.line or 0) > 0 and node.source is not None + + +class source_locations(nodes.General, nodes.Element): + """Index of all known source locations.""" + + +def visit_source_locations(self, node): + source_index: Dict[int, Tuple[str, int]] = node["index"] + + self.body.append('") + + +def depart_source_locations(self, node): ... + + +class SourceLocationTransform(Transform): + """Add source location information to the doctree. + + Used to support features like synchronised scrolling. + """ + + default_priority = 500 + + def apply(self, **kwargs): + current_line = 0 + current_source = None + + source_index = {} + source_nodes = self.document.traverse(condition=has_source) + + for idx, node in enumerate(source_nodes): + if node.line > current_line or node.source != current_source: + uri, linum = source_to_uri_and_linum(f"{node.source}:{node.line}") + + if uri is None or linum is None: + continue + + source_index[idx] = (str(uri), linum) + node["classes"].extend(["esbonio-marker", f"esbonio-marker-{idx}"]) + + # Use the source and line reported by docutils. + # Just in case source_to_uri_and_linum doesn't handle things correctly + current_line = node.line + current_source = node.source + + self.document.children.append(source_locations("", index=source_index)) + + +def setup(app: Sphinx): + # Inline the JS code we need to enable sync scrolling. + # + # 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()) + + app.add_node( + source_locations, html=(visit_source_locations, depart_source_locations) + ) + app.add_transform(SourceLocationTransform) diff --git a/lib/esbonio/esbonio/sphinx_agent/log.py b/lib/esbonio/esbonio/sphinx_agent/log.py index 4ec7b6320..01aeca695 100644 --- a/lib/esbonio/esbonio/sphinx_agent/log.py +++ b/lib/esbonio/esbonio/sphinx_agent/log.py @@ -38,73 +38,6 @@ def __init__(self, app, *args, **kwargs): self.app = app self.diagnostics: Dict[Uri, Set[types.Diagnostic]] = {} - def get_location(self, location: str) -> Tuple[str, Optional[int]]: - if not location: - conf = pathlib.Path(self.app.confdir, "conf.py") - return (str(conf), None) - - lineno = None - path, parts = self.get_location_path(location) - - if len(parts) == 1: - try: - lineno = int(parts[0]) - except ValueError: - pass - - if len(parts) == 2 and parts[0].startswith("docstring of "): - target = parts[0].replace("docstring of ", "") - lineno = self.get_docstring_location(target, parts[1]) - - return (path, lineno) - - def get_location_path(self, location: str) -> Tuple[str, List[str]]: - """Determine the filepath from the given location.""" - - if location.startswith("internal padding before "): - location = location.replace("internal padding before ", "") - - if location.startswith("internal padding after "): - location = location.replace("internal padding after ", "") - - path, *parts = location.split(":") - - # On windows the rest of the path will be the first element of parts - if pathlib.Path(location).drive: - path += f":{parts.pop(0)}" - - # Diagnostics in .. included:: files are reported relative to the process' - # working directory, so ensure the path is absolute. - path = os.path.abspath(path) - - return path, parts - - def get_docstring_location(self, target: str, offset: str) -> Optional[int]: - # The containing module will be the longest substring we can find in target - candidates = [m for m in sys.modules.keys() if target.startswith(m)] + [""] - module = sys.modules.get(sorted(candidates, key=len, reverse=True)[0], None) - - if module is None: - return None - - obj: Union[ModuleType, Any, None] = module - dotted_name = target.replace(module.__name__ + ".", "") - - for name in dotted_name.split("."): - obj = getattr(obj, name, None) - if obj is None: - return None - - try: - _, line = inspect.getsourcelines(obj) # type: ignore - - # Correct off by one error for docstrings that don't start with a newline. - nl = (obj.__doc__ or "").startswith("\n") - return line + int(offset) - (not nl) - except Exception: - logger.debug("Unable to determine diagnostic location\n%s", exc_info=True) - return None - def filter(self, record: logging.LogRecord) -> bool: conditions = [ "sphinx" not in record.name, @@ -115,7 +48,12 @@ def filter(self, record: logging.LogRecord) -> bool: return True loc = getattr(record, "location", "") - doc, lineno = self.get_location(loc) + uri, lineno = source_to_uri_and_linum(loc) + + if uri is None: + conf = pathlib.Path(self.app.confdir, "conf.py") + uri, lineno = (Uri.for_file(conf), None) + line = lineno or 1 try: @@ -144,5 +82,88 @@ def filter(self, record: logging.LogRecord) -> bool: ), ) - self.diagnostics.setdefault(Uri.for_file(doc), set()).add(diagnostic) + self.diagnostics.setdefault(uri, set()).add(diagnostic) return True + + +def source_to_uri_and_linum( + location: Optional[str], +) -> Tuple[Optional[Uri], Optional[int]]: + """Convert the given source location to a uri and corresponding line number + + Parameters + ---------- + location + The location to convert + + Returns + ------- + Tuple[Optional[Uri], Optional[int]] + The corresponding uri and line number, if known + """ + if location is None: + return None, None + + lineno = None + path, parts = _get_location_path(location) + + if len(parts) == 1: + try: + lineno = int(parts[0]) + except ValueError: + pass + + if len(parts) == 2 and parts[0].startswith("docstring of "): + target = parts[0].replace("docstring of ", "") + lineno = _get_docstring_linum(target, parts[1]) + + return Uri.for_file(path), lineno + + +def _get_location_path(location: str) -> Tuple[str, List[str]]: + """Determine the filepath from the given location.""" + + if location.startswith("internal padding before "): + location = location.replace("internal padding before ", "") + + if location.startswith("internal padding after "): + location = location.replace("internal padding after ", "") + + path, *parts = location.split(":") + + # On windows the rest of the path will be the first element of parts + if pathlib.Path(location).drive: + path += f":{parts.pop(0)}" + + # Diagnostics in .. included:: files are reported relative to the process' + # working directory, so ensure the path is absolute. + path = os.path.abspath(path) + + return path, parts + + +def _get_docstring_linum(target: str, offset: str) -> Optional[int]: + # The containing module will be the longest substring we can find in target + candidates = [m for m in sys.modules.keys() if target.startswith(m)] + [""] + module = sys.modules.get(sorted(candidates, key=len, reverse=True)[0], None) + + if module is None: + return None + + obj: Union[ModuleType, Any, None] = module + dotted_name = target.replace(module.__name__ + ".", "") + + for name in dotted_name.split("."): + obj = getattr(obj, name, None) + if obj is None: + return None + + try: + _, line = inspect.getsourcelines(obj) # type: ignore + + # Correct off by one error for docstrings that don't start with a newline. + nl = (obj.__doc__ or "").startswith("\n") + return line + int(offset) - (not nl) + except Exception: + logger.debug("Unable to determine diagnostic location\n%s", exc_info=True) + return None diff --git a/lib/esbonio/esbonio/sphinx_agent/patches.py b/lib/esbonio/esbonio/sphinx_agent/patches.py index 71e26af2a..83e28b5cf 100644 --- a/lib/esbonio/esbonio/sphinx_agent/patches.py +++ b/lib/esbonio/esbonio/sphinx_agent/patches.py @@ -68,7 +68,7 @@ def status_iterator( class progress_message: """Used to override Spinx's version of this, reports progress to the client.""" - def __init__(self, message: str) -> None: + def __init__(self, message: str, *, nonl: bool = True) -> None: self.message = message def __enter__(self) -> None: diff --git a/lib/esbonio/esbonio/sphinx_agent/static/webview.js b/lib/esbonio/esbonio/sphinx_agent/static/webview.js index e9e3f6d91..c2642ea57 100644 --- a/lib/esbonio/esbonio/sphinx_agent/static/webview.js +++ b/lib/esbonio/esbonio/sphinx_agent/static/webview.js @@ -1,45 +1,116 @@ // 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() { - let targets = new Map() - for (let target of Array.from(document.querySelectorAll(".linemarker"))) { - - let linum - for (let cls of target.classList) { - let result = cls.match(/linemarker-(\d+)/) - if (result) { - linum = parseInt(result[1]) - targets.set(linum, target) - break - } + +/** + * Find the uri and line number the editor should scroll to + * + * @returns {[string, number]} - The uri and line number + */ +function findEditorScrollTarget() { + const markers = document.querySelectorAll(".esbonio-marker") + + for (let marker of markers) { + const bbox = marker.getBoundingClientRect() + // TODO: This probably needs to be made smarter as it does not account + // for elements that are technically on screen but hidden. - e.g. by furo's header bar. + if (bbox.top < 60) { + continue + } + + const match = marker.className.match(/.* esbonio-marker-(\d+).*/) + if (!match || !match[1]) { + console.debug(`Unable to find marker id in '${marker.className}'`) + return } + + const markerId = match[1] + const location = document.querySelector(`#esbonio-marker-index span[data-id="${markerId}"]`) + if (!location) { + console.debug(`Unable to locate source for marker id: '${markerId}'`) + return + } + + const uri = location.dataset.uri + const line = parseInt(location.dataset.line) + return [uri, line] } - return targets + return } -// Return the line number we should ask the editor to scroll to. -function findScrollTarget() { +/** + * Scroll the webview to show the given location + * + * @param {string} uri - The uri of the document to reveal + * @param {number} linum - The line number within that document to reveal + */ +function scrollViewTo(uri, linum) { + + // Select all the markers with the given uri. + const markers = Array.from( + document.querySelectorAll(`#esbonio-marker-index span[data-uri="${uri}"]`) + ) - // Are we at the top of the page? - if (window.scrollY <= 100) { - return -1 + if (!markers) { + return } - for (let [linum, target] of scrollTargets.entries()) { - const bbox = target.getBoundingClientRect() - // TODO: This probably needs to be made smarter as it does not account - // for elements that are technically on screen but hidden. - e.g. by furo's header bar. - if (bbox.top > 0) { - return linum + /** @type {HTMLElement} */ + let current + + /** @type {number} */ + let currentLine = 0 + + /** @type {HTMLElement} */ + let previous + + /** @type {number} */ + let previousLine + + for (let marker of markers) { + let markerId = marker.dataset.id + let markerLine = parseInt(marker.dataset.line) + let element = document.querySelector(`.esbonio-marker-${markerId}`) + + // Only consider markers that correspond with an element currently in the DOM + if (!element) { + continue + } + + current = element + currentLine = markerLine + + // Have we passed the target line number? + if (markerLine > linum) { + break } + + previous = current + previousLine = currentLine } - return -} + if (!current) { + return + } + + if (!previous) { + previous = current + previousLine = currentLine + } + + // Scroll the view to a position that is an interpolation between the previous and + // current marker based on the requested line number. + const previousPos = window.scrollY + previous.getBoundingClientRect().top + const currentPos = window.scrollY + current.getBoundingClientRect().top -let scrollTargets = new Map() + const t = (linum - previousLine) / Math.max(currentLine - previousLine, 1) + const y = (1 - t) * previousPos + t * currentPos + + console.table({line: linum, previous: previousLine, current: currentLine, t: t, y: y}) + + window.scrollTo(0, y - 60) +} const host = window.location.hostname; const queryString = window.location.search; @@ -67,20 +138,7 @@ const handlers = { console.debug("Reloading page...") window.location.reload() }, - "view/scroll": function (params) { - if (params.line <= 1) { - window.scrollTo(0, 0) - return - } - - // TODO: Look for targets within X of target line instead? - let target = scrollTargets.get(params.line) - if (!target) { - return - } - - target.scrollIntoView(true) - } + "view/scroll": (params) => {scrollViewTo(params.uri, params.line)} } function handle(message) { @@ -91,14 +149,15 @@ function handle(message) { console.error(`Error: ${JSON.stringify(message.error, undefined, 2)}`) } else if (message.method) { let method = message.method - console.debug(`Got request: ${method}, ${JSON.stringify(params, undefined, 2)}`) + console.debug(`Request: ${method}, ${JSON.stringify(params, undefined, 2)}`) } else { let result = message.result - console.debug(`Got response: ${JSON.stringify(result, undefined, 2)}`) + console.debug(`Response: ${JSON.stringify(result, undefined, 2)}`) } } else { let handler = handlers[message.method] if (handler) { + // console.debug(`Notification: ${message.method}, ${JSON.stringify(message.params)} `) handler(message.params) } else { console.error(`Got unknown notification: '${message.method}'`) @@ -107,13 +166,23 @@ function handle(message) { } window.addEventListener("scroll", (event) => { - let linum = findScrollTarget() - if (linum) { - // TODO: Rate limits. - sendMessage( - { jsonrpc: "2.0", method: "editor/scroll", params: { line: linum } } - ) + const target = findEditorScrollTarget() + if (!target) { + return + } + + const uri = target[0] + const line = target[1] + + if (!uri || !line) { + return } + + // TODO: Rate limits. + sendMessage( + { jsonrpc: "2.0", method: "editor/scroll", params: { uri: uri, line: line } } + ) + }) // Connection opened @@ -128,9 +197,6 @@ socket.addEventListener("message", (event) => { }); function main() { - scrollTargets = indexScrollTargets() - console.debug(scrollTargets) - if (showMarkers) { let markerStyle = document.createElement('style') let lines = [".linemarker { background: rgb(255, 0, 0, 0.25); position: relative; }"] diff --git a/lib/esbonio/esbonio/sphinx_agent/transforms.py b/lib/esbonio/esbonio/sphinx_agent/transforms.py deleted file mode 100644 index a3dfd4623..000000000 --- a/lib/esbonio/esbonio/sphinx_agent/transforms.py +++ /dev/null @@ -1,17 +0,0 @@ -from docutils import nodes -from docutils.transforms import Transform - - -class LineNumberTransform(Transform): - """Adds line number markers to html nodes. - - Used to implement sync scrolling - """ - - default_priority = 500 - - def apply(self, **kwargs): - for node in self.document.traverse(nodes.paragraph): - if node.line: - node["classes"].append("linemarker") - node["classes"].append(f"linemarker-{node.line}") diff --git a/lib/esbonio/esbonio/sphinx_agent/types/roles.py b/lib/esbonio/esbonio/sphinx_agent/types/roles.py index 0cda961a6..6be914712 100644 --- a/lib/esbonio/esbonio/sphinx_agent/types/roles.py +++ b/lib/esbonio/esbonio/sphinx_agent/types/roles.py @@ -26,7 +26,7 @@ ([^\w`]|^\s*) # roles cannot be preceeded by letter chars (?P { # roles start with a '{' - (?P[:\w-]+)? # roles have a name + (?P[:\w+-]+)? # roles have a name }? # roles end with a '}' ) (?P @@ -74,7 +74,7 @@ (?P : # roles begin with a ':' character (?!:) # the next character cannot be a ':' - ((?P\w([:\w-]*\w)?):?)? # roles have a name + ((?P\w([:\w+-]*\w)?):?)? # roles have a name ) (?P ` # targets begin with a '`' character diff --git a/lib/esbonio/hatch.toml b/lib/esbonio/hatch.toml index ea7b68c8b..31c4bec51 100644 --- a/lib/esbonio/hatch.toml +++ b/lib/esbonio/hatch.toml @@ -35,4 +35,4 @@ value = ["tests/sphinx-agent", "tests/e2e"] [envs.hatch-static-analysis] config-path = "ruff_defaults.toml" -dependencies = ["ruff==0.4.7"] +dependencies = ["ruff==0.5.2"] diff --git a/lib/esbonio/ruff_defaults.toml b/lib/esbonio/ruff_defaults.toml index 18aaec501..a433be676 100644 --- a/lib/esbonio/ruff_defaults.toml +++ b/lib/esbonio/ruff_defaults.toml @@ -14,9 +14,11 @@ select = [ "ARG003", "ARG004", "ARG005", - "ASYNC100", - "ASYNC101", - "ASYNC102", + "ASYNC210", + "ASYNC220", + "ASYNC221", + "ASYNC230", + "ASYNC251", "B002", "B003", "B004", @@ -463,11 +465,6 @@ select = [ "TID251", "TID252", "TID253", - "TRIO100", - "TRIO105", - "TRIO109", - "TRIO110", - "TRIO115", "TRY002", "TRY003", "TRY004", @@ -534,17 +531,8 @@ select = [ ] [lint.per-file-ignores] -"**/scripts/*" = [ - "INP001", - "T201", -] -"**/tests/**/*" = [ - "PLC1901", - "PLR2004", - "PLR6301", - "S", - "TID252", -] +"**/scripts/*" = ["INP001", "T201"] +"**/tests/**/*" = ["PLC1901", "PLR2004", "PLR6301", "S", "TID252"] [lint.flake8-tidy-imports] ban-relative-imports = "all" diff --git a/lib/esbonio/tests/conftest.py b/lib/esbonio/tests/conftest.py index 4359fde14..3ff5e9015 100644 --- a/lib/esbonio/tests/conftest.py +++ b/lib/esbonio/tests/conftest.py @@ -7,6 +7,18 @@ TEST_DIR = pathlib.Path(__file__).parent +def pytest_addoption(parser): + """Add additional cli arguments to pytest.""" + + group = parser.getgroup("esbonio") + group.addoption( + "--enable-devtools", + dest="enable_devtools", + action="store_true", + help="enable lsp-devtools integrations", + ) + + @pytest.fixture(scope="session") def uri_for(): """Helper function for returning the uri for a given file in the ``tests/`` diff --git a/lib/esbonio/tests/e2e/test_e2e_roles.py b/lib/esbonio/tests/e2e/test_e2e_roles.py index 81b6049d4..1ad20590c 100644 --- a/lib/esbonio/tests/e2e/test_e2e_roles.py +++ b/lib/esbonio/tests/e2e/test_e2e_roles.py @@ -32,6 +32,14 @@ } +LOCAL_PY_CLASSES = { + "counters.pattern.PatternCounter", + "counters.pattern.NoMatchesError", +} +PYTHON_PY_CLASSES = {"logging.Filter", "http.server.HTTPServer"} +SPHINX_PY_CLASSES = {"sphinx.addnodes.desc"} + + @pytest.mark.parametrize( "text, expected, unexpected", [ @@ -131,13 +139,28 @@ async def test_rst_role_completions( (":std:doc:`", {"demo_myst", "demo_rst", "rst/domains/python"}, set()), ( ":class:`", - {"counters.pattern.PatternCounter", "counters.pattern.NoMatchesError"}, - set(), + LOCAL_PY_CLASSES, + PYTHON_PY_CLASSES | SPHINX_PY_CLASSES, ), ( ":py:class:`", - {"counters.pattern.PatternCounter", "counters.pattern.NoMatchesError"}, - set(), + LOCAL_PY_CLASSES, + PYTHON_PY_CLASSES | SPHINX_PY_CLASSES, + ), + ( + ":external:py:class:`", + PYTHON_PY_CLASSES | SPHINX_PY_CLASSES, + LOCAL_PY_CLASSES, + ), + ( + ":external+python:py:class:`", + PYTHON_PY_CLASSES, + LOCAL_PY_CLASSES | SPHINX_PY_CLASSES, + ), + ( + ":external+sphinx:py:class:`", + SPHINX_PY_CLASSES, + LOCAL_PY_CLASSES | PYTHON_PY_CLASSES, ), (":func:`", {"counters.pattern.count_numbers"}, set()), (":py:func:`", {"counters.pattern.count_numbers"}, set()), @@ -313,13 +336,28 @@ async def test_myst_role_completions( ("{std:doc}`", {"demo_myst", "demo_rst", "rst/domains/python"}, set()), ( "{class}`", - {"counters.pattern.PatternCounter", "counters.pattern.NoMatchesError"}, - set(), + LOCAL_PY_CLASSES, + PYTHON_PY_CLASSES | SPHINX_PY_CLASSES, ), ( "{py:class}`", - {"counters.pattern.PatternCounter", "counters.pattern.NoMatchesError"}, - set(), + LOCAL_PY_CLASSES, + PYTHON_PY_CLASSES | SPHINX_PY_CLASSES, + ), + ( + "{external:py:class}`", + PYTHON_PY_CLASSES | SPHINX_PY_CLASSES, + LOCAL_PY_CLASSES, + ), + ( + "{external+sphinx:py:class}`", + SPHINX_PY_CLASSES, + LOCAL_PY_CLASSES | PYTHON_PY_CLASSES, + ), + ( + "{external+python:py:class}`", + PYTHON_PY_CLASSES, + LOCAL_PY_CLASSES | SPHINX_PY_CLASSES, ), ("{func}`", {"counters.pattern.count_numbers"}, set()), ("{py:func}`", {"counters.pattern.count_numbers"}, set()), diff --git a/lib/esbonio/tests/server/features/test_sphinx_config.py b/lib/esbonio/tests/server/features/test_sphinx_config.py index 918ac9a64..96e3bdd5f 100644 --- a/lib/esbonio/tests/server/features/test_sphinx_config.py +++ b/lib/esbonio/tests/server/features/test_sphinx_config.py @@ -175,4 +175,5 @@ def test_resolve( expected The expected outcome """ - assert config.resolve(Uri.parse(uri), workspace, logger) == expected + actual = config.resolve(Uri.parse(uri), workspace, logger) + assert actual == expected diff --git a/lib/esbonio/tests/server/test_configuration.py b/lib/esbonio/tests/server/test_configuration.py index 2480a11c6..237b86cf8 100644 --- a/lib/esbonio/tests/server/test_configuration.py +++ b/lib/esbonio/tests/server/test_configuration.py @@ -26,6 +26,12 @@ class ExampleConfig: log_names: List[str] = attrs.field(factory=list) +@attrs.define +class ColorConfig: + color: str + scope: str = attrs.field(default="${scope}") + + @pytest.fixture def server(event_loop): """Return a server instance for testing.""" @@ -234,6 +240,188 @@ def server(event_loop): id="workspace-file-override[win]", marks=pytest.mark.skipif(not IS_WIN, reason="windows only"), ), + pytest.param( # Check that we can expand config variables correctly + {}, + { + "file:///path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + {}, + "esbonio.colors", + ColorConfig, + "file:///path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="file:///path/to/workspace"), + id="scope-variable[workspace][unix]", + marks=pytest.mark.skipif(IS_WIN, reason="windows"), + ), + pytest.param( # Check that we can expand config variables correctly + {}, + { + "file:///c%3A/path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + {}, + "esbonio.colors", + ColorConfig, + "file:///c:/path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="file:///c%3A/path/to/workspace"), + id="scope-variable[workspace][win]", + marks=pytest.mark.skipif(not IS_WIN, reason="windows only"), + ), + pytest.param( # Check that we can expand config variables correctly + {}, + { + "file:///path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + { + "file:///path/to/workspace/docs": dict( + esbonio=dict(colors=dict(color="blue")) + ), + }, + "esbonio.colors", + ColorConfig, + "file:///path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="file:///path/to/workspace/docs"), + id="scope-variable[workspace+file][unix]", + marks=pytest.mark.skipif(IS_WIN, reason="windows"), + ), + pytest.param( # Check that we can expand config variables correctly + {}, + { + "file:///c%3A/path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + { + "file:///c%3A/path/to/workspace/docs": dict( + esbonio=dict(colors=dict(color="blue")) + ), + }, + "esbonio.colors", + ColorConfig, + "file:///c:/path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="file:///c%3A/path/to/workspace/docs"), + id="scope-variable[workspace+file][win]", + marks=pytest.mark.skipif(not IS_WIN, reason="windows only"), + ), + pytest.param( # The user should still be able to override them + {}, + { + "file:///path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + { + "file:///path/to/workspace/docs": dict( + esbonio=dict(colors=dict(color="blue", scope="file:///my/scope")) + ), + }, + "esbonio.colors", + ColorConfig, + "file:///path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="file:///my/scope"), + id="scope-variable-override[unix]", + marks=pytest.mark.skipif(IS_WIN, reason="windows"), + ), + pytest.param( # The user should still be able to override them + {}, + { + "file:///c%3A/path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + { + "file:///c%3A/path/to/workspace/docs": dict( + esbonio=dict(colors=dict(color="blue", scope="file:///my/scope")) + ), + }, + "esbonio.colors", + ColorConfig, + "file:///c:/path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="file:///my/scope"), + id="scope-variable-override[win]", + marks=pytest.mark.skipif(not IS_WIN, reason="windows only"), + ), + pytest.param( + {}, + { + "file:///path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + { + "file:///path/to/workspace/docs": dict( + esbonio=dict(colors=dict(color="blue", scope="${scopePath}")) + ), + }, + "esbonio.colors", + ColorConfig, + "file:///path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="/path/to/workspace/docs"), + id="scope-path-variable[unix]", + marks=pytest.mark.skipif(IS_WIN, reason="windows"), + ), + pytest.param( + {}, + { + "file:///c%3A/path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + { + "file:///c%3A/path/to/workspace/docs": dict( + esbonio=dict(colors=dict(color="blue", scope="${scopePath}")) + ), + }, + "esbonio.colors", + ColorConfig, + "file:///c:/path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="/c:/path/to/workspace/docs"), + id="scope-path-variable[win]", + marks=pytest.mark.skipif(not IS_WIN, reason="windows only"), + ), + pytest.param( + {}, + { + "file:///path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + { + "file:///path/to/workspace/docs": dict( + esbonio=dict(colors=dict(color="blue", scope="${scopeFsPath}")) + ), + }, + "esbonio.colors", + ColorConfig, + "file:///path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="/path/to/workspace/docs"), + id="scope-fspath-variable[unix]", + marks=pytest.mark.skipif(IS_WIN, reason="windows"), + ), + pytest.param( + {}, + { + "file:///c%3A/path/to/workspace": dict( + esbonio=dict(colors=dict(color="red")) + ), + }, + { + "file:///c%3A/path/to/workspace/docs": dict( + esbonio=dict(colors=dict(color="blue", scope="${scopeFsPath}")) + ), + }, + "esbonio.colors", + ColorConfig, + "file:///c:/path/to/workspace/docs/file.txt", + ColorConfig(color="red", scope="c:\\path\\to\\workspace\\docs"), + id="scope-fspath-variable[win]", + marks=pytest.mark.skipif(not IS_WIN, reason="windows only"), + ), ], ) def test_get_configuration( diff --git a/lib/esbonio/tests/server/test_patterns.py b/lib/esbonio/tests/server/test_patterns.py index 3d6747670..722ce0e90 100644 --- a/lib/esbonio/tests/server/test_patterns.py +++ b/lib/esbonio/tests/server/test_patterns.py @@ -73,6 +73,14 @@ def test_myst_directive_regex(string, expected): ("{code-block", {"name": "code-block", "role": "{code-block"}), ("{c:func}", {"name": "c:func", "role": "{c:func}"}), ("{cpp:func}", {"name": "cpp:func", "role": "{cpp:func}"}), + ( + "{external:cpp:func}", + {"name": "external:cpp:func", "role": "{external:cpp:func}"}, + ), + ( + "{external+python:cpp:func}", + {"name": "external+python:cpp:func", "role": "{external+python:cpp:func}"}, + ), ("{ref}`", {"name": "ref", "role": "{ref}", "target": "`"}), ( "{code-block}`", @@ -532,6 +540,14 @@ def test_directive_option_regex(string, expected): (":code-block", {"name": "code-block", "role": ":code-block"}), (":c:func:", {"name": "c:func", "role": ":c:func:"}), (":cpp:func:", {"name": "cpp:func", "role": ":cpp:func:"}), + ( + ":external:cpp:func:", + {"name": "external:cpp:func", "role": ":external:cpp:func:"}, + ), + ( + ":external+python:cpp:func:", + {"name": "external+python:cpp:func", "role": ":external+python:cpp:func:"}, + ), (":ref:`", {"name": "ref", "role": ":ref:", "target": "`"}), ( ":code-block:`", diff --git a/lib/esbonio/tests/sphinx-agent/conftest.py b/lib/esbonio/tests/sphinx-agent/conftest.py index 071072f71..2e43d085d 100644 --- a/lib/esbonio/tests/sphinx-agent/conftest.py +++ b/lib/esbonio/tests/sphinx-agent/conftest.py @@ -7,7 +7,6 @@ from lsprotocol.types import WorkspaceFolder from pygls.protocol import default_converter from pygls.workspace import Workspace -from sphinx.application import Sphinx from esbonio.server.features.project_manager import Project from esbonio.server.features.sphinx_manager.client import ClientState @@ -18,17 +17,22 @@ make_test_sphinx_client, ) from esbonio.server.features.sphinx_manager.config import SphinxConfig +from esbonio.server.features.sphinx_manager.config import get_module_path +from esbonio.sphinx_agent.app import Sphinx logger = logging.getLogger(__name__) @pytest.fixture def build_dir(tmp_path_factory): - return tmp_path_factory.mktemp("build") + _dir = tmp_path_factory.mktemp("build") + print(f"Using build dir: {_dir}") + + return _dir @pytest_asyncio.fixture -async def client(uri_for, build_dir): +async def client(request, uri_for, build_dir): demo_workspace = uri_for("workspaces", "demo") test_uri = demo_workspace / "index.rst" @@ -39,6 +43,7 @@ async def client(uri_for, build_dir): ], ) config = SphinxConfig( + enable_dev_tools=request.config.getoption("enable_devtools"), python_command=[sys.executable], build_command=[ "sphinx-build", @@ -63,7 +68,15 @@ async def client(uri_for, build_dir): @pytest.fixture def app(client, build_dir): """Sphinx application instance, used for validating results.""" - return Sphinx( + + # In order to load the pickled envrionment correctly, we need to temporarily put + # the parent directory of the `sphinx_agent` module on the path. + path = get_module_path("esbonio.sphinx_agent") + assert path is not None + + sys.path.insert(0, str(path)) + + _app = Sphinx( srcdir=client.sphinx_info.src_dir, confdir=client.sphinx_info.conf_dir, outdir=str(pathlib.Path(build_dir, "html")), @@ -71,6 +84,10 @@ def app(client, build_dir): buildername="html", ) + sys.path.pop(0) + + return _app + @pytest_asyncio.fixture async def project(client: SubprocessSphinxClient): diff --git a/lib/esbonio/tests/sphinx-agent/handlers/test_domains.py b/lib/esbonio/tests/sphinx-agent/handlers/test_domains.py index e1c07c5fc..ffa5788a2 100644 --- a/lib/esbonio/tests/sphinx-agent/handlers/test_domains.py +++ b/lib/esbonio/tests/sphinx-agent/handlers/test_domains.py @@ -7,6 +7,8 @@ from esbonio.server.features.project_manager import Project if typing.TYPE_CHECKING: + from typing import List + from sphinx.application import Sphinx @@ -23,7 +25,8 @@ async def test_python_domain_discovery(app: Sphinx, project: Project): actual = set() db = await project.get_db() cursor = await db.execute( - "SELECT name, objtype, docname FROM objects where domain = 'py'" + "SELECT name, objtype, docname FROM objects " + "WHERE domain = 'py' AND project IS NULL" ) for item in await cursor.fetchall(): @@ -45,10 +48,39 @@ async def test_std_domain_discovery(app: Sphinx, project: Project): actual = set() db = await project.get_db() cursor = await db.execute( - "SELECT name, objtype, docname FROM objects where domain = 'std'" + "SELECT name, objtype, docname FROM objects " + "WHERE domain = 'std' AND project IS NULL" ) for item in await cursor.fetchall(): actual.add(item) assert expected == actual + + +@pytest.mark.parametrize( + "projname, domain, objtype, expected", + [ + ("python", "py", "class", ["logging.Filter"]), + ("sphinx", "py", "class", ["sphinx.addnodes.desc"]), + ], +) +@pytest.mark.asyncio +async def test_intersphinx_domain_discovery( + project: Project, domain: str, objtype: str, projname: str, expected: List[str] +): + """Ensure that we can correctly index all the objects associated with the Python + domain in intersphinx projects.""" + + db = await project.get_db() + cursor = await db.execute( + "SELECT name FROM objects " "WHERE domain = ? AND objtype = ? AND project = ?", + (domain, objtype, projname), + ) + + actual = set() + for (name,) in await cursor.fetchall(): + actual.add(name) + + for name in expected: + assert name in actual diff --git a/lib/esbonio/tests/sphinx-agent/test_sa_unit.py b/lib/esbonio/tests/sphinx-agent/test_sa_unit.py index 65462879f..6f83c7f8b 100644 --- a/lib/esbonio/tests/sphinx-agent/test_sa_unit.py +++ b/lib/esbonio/tests/sphinx-agent/test_sa_unit.py @@ -11,6 +11,8 @@ from esbonio.sphinx_agent.config import SphinxConfig from esbonio.sphinx_agent.log import DiagnosticFilter +from esbonio.sphinx_agent.log import source_to_uri_and_linum +from esbonio.sphinx_agent.types import Uri if typing.TYPE_CHECKING: from typing import Any @@ -457,30 +459,24 @@ def test_cli_arg_handling(args: List[str], expected: Dict[str, Any]): @pytest.mark.parametrize( "location, expected", [ - ("", (str(CONF_PATH), None)), - (f"{RST_PATH}", (str(RST_PATH), None)), - (f"{RST_PATH}:", (str(RST_PATH), None)), - (f"{RST_PATH}:3", (str(RST_PATH), 3)), - (f"{REL_INC_PATH}:12", (str(INC_PATH), 12)), + (f"{RST_PATH}", (Uri.for_file(RST_PATH), None)), + (f"{RST_PATH}:", (Uri.for_file(RST_PATH), None)), + (f"{RST_PATH}:3", (Uri.for_file(RST_PATH), 3)), + (f"{REL_INC_PATH}:12", (Uri.for_file(INC_PATH), 12)), ( f"{PY_PATH}:docstring of esbonio.sphinx_agent.log.DiagnosticFilter:3", - (str(PY_PATH), 22), + (Uri.for_file(PY_PATH), 22), ), - (f"internal padding after {RST_PATH}:34", (str(RST_PATH), 34)), - (f"internal padding before {RST_PATH}:34", (str(RST_PATH), 34)), + (f"internal padding after {RST_PATH}:34", (Uri.for_file(RST_PATH), 34)), + (f"internal padding before {RST_PATH}:34", (Uri.for_file(RST_PATH), 34)), ], ) -def test_get_diagnostic_location(location: str, expected: Tuple[str, Optional[int]]): +def test_source_to_uri_linum(location: str, expected: Tuple[str, Optional[int]]): """Ensure we can correctly determine a dianostic's location based on the string we get from sphinx.""" - app = mock.Mock() - app.confdir = str(ROOT / "sphinx-extensions") - - handler = DiagnosticFilter(app) - mockpath = f"{DiagnosticFilter.__module__}.inspect.getsourcelines" with mock.patch(mockpath, return_value=([""], 20)): - actual = handler.get_location(location) + actual = source_to_uri_and_linum(location) assert actual == expected diff --git a/lib/esbonio/tests/workspaces/demo/conf.py b/lib/esbonio/tests/workspaces/demo/conf.py index bbd74a0ee..cd49e88cb 100644 --- a/lib/esbonio/tests/workspaces/demo/conf.py +++ b/lib/esbonio/tests/workspaces/demo/conf.py @@ -17,6 +17,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ + "sphinx.ext.intersphinx", "sphinx_design", "myst_parser", ] @@ -24,6 +25,16 @@ templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "env", ".tox", "README.md"] +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master", None), +} + + +linkcheck_allowed_redirects = { + # All HTTP redirections from the source URI to the canonical URI will be treated as "working". + r"https://sphinx-doc\.org/.*": r"https://sphinx-doc\.org/en/master/.*" +} # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/scripts/sphinx-app.py b/scripts/sphinx-app.py index f7fa996d3..8bb0aadf4 100644 --- a/scripts/sphinx-app.py +++ b/scripts/sphinx-app.py @@ -35,6 +35,7 @@ import pathlib import pdb import time +import traceback from esbonio.sphinx_agent import handlers, types from esbonio.sphinx_agent.app import Sphinx @@ -53,4 +54,5 @@ ) app.build() except Exception: + traceback.print_exc() pdb.post_mortem()