From a51d4427101f45fd8cbff9def0421382182e683f Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 11 Apr 2022 18:27:31 +0000
Subject: [PATCH 01/12] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0)
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index de843be18..10a21355d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: '.bumpversion.cfg$'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.1.0
+ rev: v4.2.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
From 9cf45a2ae477157da8140c74f943f75df7d9cfa4 Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Fri, 15 Apr 2022 00:29:20 +0100
Subject: [PATCH 02/12] code: Bump dependencies
---
code/package-lock.json | 28 ++++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/code/package-lock.json b/code/package-lock.json
index f3058e663..006497440 100644
--- a/code/package-lock.json
+++ b/code/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "esbonio",
- "version": "0.7.3",
+ "version": "0.8.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "esbonio",
- "version": "0.7.3",
+ "version": "0.8.1",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5",
@@ -388,9 +388,9 @@
}
},
"node_modules/ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
+ "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
"dev": true,
"engines": {
"node": ">=4"
@@ -2268,9 +2268,9 @@
}
},
"node_modules/minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"node_modules/mkdirp": {
@@ -4421,9 +4421,9 @@
"dev": true
},
"ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
+ "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
"dev": true
},
"ansi-styles": {
@@ -5815,9 +5815,9 @@
}
},
"minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"mkdirp": {
From 0e82eeb06522fb1cb98e8088a577ddb3b8cfbae8 Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Sat, 16 Apr 2022 23:32:06 +0100
Subject: [PATCH 03/12] lsp: Lookup `:ref:` targets by name
It turns out that when docutils transforms a label into the id used in
the parsed doctree, it also maintains a mapping of the "names" that
correspond to these ids.
To ensure that the language server correctly handles all forms of label
it now uses this map to look up the correct id to search for.
---
lib/esbonio/changes/357.fix.rst | 1 +
lib/esbonio/esbonio/lsp/sphinx/domains.py | 2 +-
.../tests/sphinx-default/test_sd_sphinx_domains.py | 11 +++++++++++
.../tests/sphinx-default/workspace/definitions.rst | 2 ++
lib/esbonio/tests/sphinx-default/workspace/index.rst | 2 ++
5 files changed, 17 insertions(+), 1 deletion(-)
create mode 100644 lib/esbonio/changes/357.fix.rst
diff --git a/lib/esbonio/changes/357.fix.rst b/lib/esbonio/changes/357.fix.rst
new file mode 100644
index 000000000..19ff26031
--- /dev/null
+++ b/lib/esbonio/changes/357.fix.rst
@@ -0,0 +1 @@
+Goto definition for ``:ref:`` targets now works for labels containing ``-`` characters
diff --git a/lib/esbonio/esbonio/lsp/sphinx/domains.py b/lib/esbonio/esbonio/lsp/sphinx/domains.py
index 7528b0a23..3f3a8479d 100644
--- a/lib/esbonio/esbonio/lsp/sphinx/domains.py
+++ b/lib/esbonio/esbonio/lsp/sphinx/domains.py
@@ -138,7 +138,7 @@ def ref_definition(self, label: str) -> List[Location]:
if "refid" not in node:
continue
- if label == node["refid"].replace("-", "_"):
+ if doctree.nameids.get(label, "") == node["refid"]:
uri = Uri.from_fs_path(node.source)
line = node.line
break
diff --git a/lib/esbonio/tests/sphinx-default/test_sd_sphinx_domains.py b/lib/esbonio/tests/sphinx-default/test_sd_sphinx_domains.py
index b5570f9ba..a435feb3b 100644
--- a/lib/esbonio/tests/sphinx-default/test_sd_sphinx_domains.py
+++ b/lib/esbonio/tests/sphinx-default/test_sd_sphinx_domains.py
@@ -272,6 +272,17 @@ async def test_role_target_completions(client: Client, text: str, setup):
),
),
),
+ (
+ "definitions.rst",
+ Position(line=29, character=36),
+ Location(
+ uri="index.rst",
+ range=Range(
+ start=Position(line=18, character=0),
+ end=Position(line=19, character=0),
+ ),
+ ),
+ ),
],
)
async def test_role_target_definitions(client: Client, path, position, expected):
diff --git a/lib/esbonio/tests/sphinx-default/workspace/definitions.rst b/lib/esbonio/tests/sphinx-default/workspace/definitions.rst
index a0c4ab477..133d917de 100644
--- a/lib/esbonio/tests/sphinx-default/workspace/definitions.rst
+++ b/lib/esbonio/tests/sphinx-default/workspace/definitions.rst
@@ -26,3 +26,5 @@ Some literal includes now
.. image:: /_static/vscode-screenshot.png
.. figure:: /_static/bad.png
+
+This line refers to :ref:`setup-label`
diff --git a/lib/esbonio/tests/sphinx-default/workspace/index.rst b/lib/esbonio/tests/sphinx-default/workspace/index.rst
index 98c3186df..2da220500 100644
--- a/lib/esbonio/tests/sphinx-default/workspace/index.rst
+++ b/lib/esbonio/tests/sphinx-default/workspace/index.rst
@@ -16,6 +16,8 @@ Welcome to Defaults's documentation!
definitions
glossary
+.. _setup-label:
+
Setup
=====
From 312f719473c66c51b013f43ec8ddd1181b37db5b Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Sun, 17 Apr 2022 01:07:58 +0100
Subject: [PATCH 04/12] lsp: Add `sphinx.forceFullBuild` init option
---
code/changes/358.enhancement.rst | 1 +
code/package.json | 6 +++++
code/src/lsp/client.ts | 7 +++++-
docs/lsp/getting-started.rst | 25 +++++++++++--------
lib/esbonio/changes/358.enhancement.rst | 1 +
lib/esbonio/esbonio/lsp/sphinx/__init__.py | 5 +++-
.../tests/sphinx-default/test_sd_sphinx.py | 4 +--
7 files changed, 34 insertions(+), 15 deletions(-)
create mode 100644 code/changes/358.enhancement.rst
create mode 100644 lib/esbonio/changes/358.enhancement.rst
diff --git a/code/changes/358.enhancement.rst b/code/changes/358.enhancement.rst
new file mode 100644
index 000000000..3ab6ee030
--- /dev/null
+++ b/code/changes/358.enhancement.rst
@@ -0,0 +1 @@
+Added the ``esbonio.sphinx.forceFullBuild`` option (default: ``true``) which can be used to control if the language server forces a full Sphinx build on startup.
diff --git a/code/package.json b/code/package.json
index 8eb2308bd..3b6ccc98c 100644
--- a/code/package.json
+++ b/code/package.json
@@ -250,6 +250,12 @@
"default": null,
"description": "The Language Server should be able to automatically find the folder containing your project's 'conf.py' file. However this setting can be used to force the Language Server to use a particular directory if required."
},
+ "esbonio.sphinx.forceFullBuild": {
+ "scope": "window",
+ "type": "boolean",
+ "default": true,
+ "description": "By default the language server will force a full build of your documentation on startup to help improve the accuracy of some features like diagnostics. Disabling this option can help improve startup time for larger projects at the expense of certain features being less accurate."
+ },
"esbonio.sphinx.srcDir": {
"scope": "window",
"type": "string",
diff --git a/code/src/lsp/client.ts b/code/src/lsp/client.ts
index fb9f54ff9..603872e49 100644
--- a/code/src/lsp/client.ts
+++ b/code/src/lsp/client.ts
@@ -43,6 +43,10 @@ export interface SphinxConfig {
*/
builderName?: string
+ /**
+ * Flag to force a full build of the documentation on startup.
+ */
+ forceFullBuild?: boolean
}
/**
@@ -296,7 +300,8 @@ export class EsbonioClient {
sphinx: {
srcDir: config.get("sphinx.srcDir"),
confDir: config.get('sphinx.confDir'),
- buildDir: buildDir
+ buildDir: buildDir,
+ forceFullBuild: config.get('sphinx.forceFullBuild')
},
server: {
logLevel: config.get('server.logLevel'),
diff --git a/docs/lsp/getting-started.rst b/docs/lsp/getting-started.rst
index f3dc89d8d..d2e2342d5 100644
--- a/docs/lsp/getting-started.rst
+++ b/docs/lsp/getting-started.rst
@@ -169,6 +169,18 @@ Configuration
.. include:: ./editors/emacs-lsp-mode/_configuration.rst
+.. confval:: sphinx.buildDir (string)
+
+ By default the language server will choose a cache directory (as determined by
+ `appdirs `_) to put Sphinx's build output.
+ This option can be used to force the language server to use a location
+ of your choosing, currently accepted values include:
+
+ - ``/path/to/src/`` - An absolute path
+ - ``${workspaceRoot}/docs/src`` - A path relative to the root of your workspace
+ - ``${workspaceFolder}/docs/src`` - Same as ``${workspaceRoot}``, placeholder for true multi-root workspace support.
+ - ``${confDir}/../src/`` - A path relative to your project's ``confDir``
+
.. confval:: sphinx.confDir (string)
The language server attempts to automatically find the folder which contains your
@@ -180,7 +192,6 @@ Configuration
- ``${workspaceRoot}/docs`` - A path relative to the root of your workspace.
- ``${workspaceFolder}/docs`` - Same as ``${workspaceRoot}``, placeholder for true multi-root workspace support.
-
.. confval:: sphinx.srcDir (string)
The language server assumes that your project's ``srcDir`` (the folder containing your
@@ -193,17 +204,9 @@ Configuration
- ``${workspaceFolder}/docs/src`` - Same as ``${workspaceRoot}``, placeholder for true multi-root workspace support.
- ``${confDir}/../src/`` - A path relative to your project's ``confDir``
-.. confval:: sphinx.buildDir (string)
+.. confval:: sphinx.forceFullBuild (boolean)
- By default the language server will choose a cache directory (as determined by
- `appdirs `_) to put Sphinx's build output.
- This option can be used to force the language server to use a location
- of your choosing, currently accepted values include:
-
- - ``/path/to/src/`` - An absolute path
- - ``${workspaceRoot}/docs/src`` - A path relative to the root of your workspace
- - ``${workspaceFolder}/docs/src`` - Same as ``${workspaceRoot}``, placeholder for true multi-root workspace support.
- - ``${confDir}/../src/`` - A path relative to your project's ``confDir``
+ Flag that indicates if the server should force a full build of the documentation on startup. (Default: ``true``)
.. confval:: server.logLevel (string)
diff --git a/lib/esbonio/changes/358.enhancement.rst b/lib/esbonio/changes/358.enhancement.rst
new file mode 100644
index 000000000..135f2c83f
--- /dev/null
+++ b/lib/esbonio/changes/358.enhancement.rst
@@ -0,0 +1 @@
+Language clients can now control if the server forces a full build of a Sphinx project on startup by providing a ``sphinx.forceFullBuild`` initialization option, which defaults to ``true``
diff --git a/lib/esbonio/esbonio/lsp/sphinx/__init__.py b/lib/esbonio/esbonio/lsp/sphinx/__init__.py
index 05e5ad3a0..19823f485 100644
--- a/lib/esbonio/esbonio/lsp/sphinx/__init__.py
+++ b/lib/esbonio/esbonio/lsp/sphinx/__init__.py
@@ -103,6 +103,9 @@ class SphinxConfig(BaseModel):
builder_name: str = Field("html", alias="builderName")
"""The currently used builder name."""
+ force_full_build: bool = Field(True, alias="forceFullBuild")
+ """Flag that can be used to force a full build on startup."""
+
class SphinxServerConfig(ServerConfig):
@@ -406,7 +409,7 @@ def create_sphinx_app(self, options: InitializationOptions) -> Optional[Sphinx]:
buildername=builder_name,
status=None, # type: ignore
warning=None, # type: ignore
- freshenv=True, # Have Sphinx reload everything on first build.
+ freshenv=sphinx.force_full_build,
)
# This has to happen after app creation otherwise our handler
diff --git a/lib/esbonio/tests/sphinx-default/test_sd_sphinx.py b/lib/esbonio/tests/sphinx-default/test_sd_sphinx.py
index 3e9f36f4e..f9e720e9f 100644
--- a/lib/esbonio/tests/sphinx-default/test_sd_sphinx.py
+++ b/lib/esbonio/tests/sphinx-default/test_sd_sphinx.py
@@ -182,14 +182,14 @@ def _(*args, **kwargs):
),
],
)
-async def test_initialization(caplog, command: List[str], path: str, options, expected):
+async def test_initialization(command: List[str], path: str, options, expected):
"""Ensure that the server responds correctly to various initialization options."""
root_path = pathlib.Path(__file__).parent / path
root_uri = uri.from_fs_path(str(root_path))
for key, value in options.dict().items():
- if key not in {"builder_name"} and value is not None:
+ if key in {"conf_dir", "src_dir", "build_dir"} and value is not None:
path = resolve_path(value, root_path)
setattr(options, key, str(path))
From 8ba77508128a8a4df949db5c15774dddb1d24623 Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Sun, 17 Apr 2022 17:49:26 +0100
Subject: [PATCH 05/12] lsp: Move xxx_dir logic into `SphinxConfig`
---
lib/esbonio/esbonio/lsp/sphinx/__init__.py | 306 ++++++++++----------
lib/esbonio/tests/unit_tests/test_sphinx.py | 26 +-
2 files changed, 158 insertions(+), 174 deletions(-)
diff --git a/lib/esbonio/esbonio/lsp/sphinx/__init__.py b/lib/esbonio/esbonio/lsp/sphinx/__init__.py
index 19823f485..b87253213 100644
--- a/lib/esbonio/esbonio/lsp/sphinx/__init__.py
+++ b/lib/esbonio/esbonio/lsp/sphinx/__init__.py
@@ -106,6 +106,147 @@ class SphinxConfig(BaseModel):
force_full_build: bool = Field(True, alias="forceFullBuild")
"""Flag that can be used to force a full build on startup."""
+ def resolve_build_dir(self, root_uri: str, actual_conf_dir: str) -> pathlib.Path:
+ """Get the build dir to use based on the user's config.
+
+ If nothing is specified in the given ``config``, this will choose a location
+ within the user's cache dir (as determined by
+ `appdirs `). The directory name will be a hash
+ derived from the given ``conf_dir`` for the project.
+
+ Alternatively the user (or least language client) can override this by setting
+ either an absolute path, or a path based on the following "variables".
+
+ - ``${workspaceRoot}`` which expands to the workspace root as provided
+ by the language client.
+ - ``${workspaceFolder}`` alias for ``${workspaceRoot}, placeholder ready for
+ multi-root support.
+ - ``${confDir}`` which expands to the configured config dir.
+
+ Parameters
+ ----------
+ root_uri
+ The workspace root uri
+
+ actual_conf_dir:
+ The fully resolved conf dir for the project
+ """
+
+ if not self.build_dir:
+ # Try to pick a sensible dir based on the project's location
+ cache = appdirs.user_cache_dir("esbonio", "swyddfa")
+ project = hashlib.md5(str(actual_conf_dir).encode()).hexdigest()
+
+ return pathlib.Path(cache) / project
+
+ root_dir = Uri.to_fs_path(root_uri)
+ match = PATH_VAR_PATTERN.match(self.build_dir)
+
+ if match and match.group(1) in {"workspaceRoot", "workspaceFolder"}:
+ build = pathlib.Path(self.build_dir).parts[1:]
+ return pathlib.Path(root_dir, *build).resolve()
+
+ if match and match.group(1) == "confDir":
+ build = pathlib.Path(self.build_dir).parts[1:]
+ return pathlib.Path(actual_conf_dir, *build).resolve()
+
+ # Convert path to/from uri so that any path quirks from windows are
+ # automatically handled
+ build_uri = Uri.from_fs_path(self.build_dir)
+ build_dir = Uri.to_fs_path(build_uri)
+
+ # But make sure paths starting with '~' are not corrupted
+ if build_dir.startswith("/~"):
+ build_dir = build_dir.replace("/~", "~")
+
+ # But make sure (windows) paths starting with '~' are not corrupted
+ if build_dir.startswith("\\~"):
+ build_dir = build_dir.replace("\\~", "~")
+
+ return pathlib.Path(build_dir).expanduser()
+
+ def resolve_conf_dir(self, root_uri: str) -> Optional[pathlib.Path]:
+ """Get the conf dir to use based on the user's config.
+
+ If ``conf_dir`` is not set, this method will attempt to find it by searching
+ within the ``root_uri`` for a ``conf.py`` file. If multiple files are found, the
+ first one found will be chosen.
+
+ If ``conf_dir`` is set the following "variables" are handled by this method
+
+ - ``${workspaceRoot}`` which expands to the workspace root as provided by the
+ language client.
+ - ``${workspaceFolder}`` alias for ``${workspaceRoot}, placeholder ready for
+ multi-root support.
+
+ Parameters
+ ----------
+ root_uri
+ The workspace root uri
+ """
+ root = Uri.to_fs_path(root_uri)
+
+ if not self.conf_dir:
+ ignore_paths = [".tox", "site-packages"]
+
+ for candidate in pathlib.Path(root).glob("**/conf.py"):
+ # Skip any files that obviously aren't part of the project
+ if any(path in str(candidate) for path in ignore_paths):
+ continue
+
+ return candidate.parent
+
+ # Nothing found
+ return None
+
+ match = PATH_VAR_PATTERN.match(self.conf_dir)
+ if not match or match.group(1) not in {"workspaceRoot", "workspaceFolder"}:
+ return pathlib.Path(self.conf_dir).expanduser()
+
+ conf = pathlib.Path(self.conf_dir).parts[1:]
+ return pathlib.Path(root, *conf).resolve()
+
+ def resolve_src_dir(self, root_uri: str, actual_conf_dir: str) -> pathlib.Path:
+ """Get the src dir to use based on the user's config.
+
+ By default the src dir will be the same as the conf dir, but this can
+ be overriden by setting the ``src_dir`` field.
+
+ There are a number of "variables" that can be included in the path,
+ currently we support
+
+ - ``${workspaceRoot}`` which expands to the workspace root as provided
+ by the language client.
+ - ``${workspaceFolder}`` alias for ``${workspaceRoot}, placeholder ready for
+ multi-root support.
+ - ``${confDir}`` which expands to the configured config dir.
+
+ Parameters
+ ----------
+ root_uri
+ The workspace root uri
+
+ actual_conf_dir
+ The fully resolved conf dir for the project
+ """
+
+ if not self.src_dir:
+ return actual_conf_dir
+
+ src_dir = self.src_dir
+ root_dir = Uri.to_fs_path(root_uri)
+
+ match = PATH_VAR_PATTERN.match(src_dir)
+ if match and match.group(1) in {"workspaceRoot", "workspaceFolder"}:
+ src = pathlib.Path(src_dir).parts[1:]
+ return pathlib.Path(root_dir, *src).resolve()
+
+ if match and match.group(1) == "confDir":
+ src = pathlib.Path(src_dir).parts[1:]
+ return pathlib.Path(actual_conf_dir, *src).resolve()
+
+ return pathlib.Path(src_dir).expanduser()
+
class SphinxServerConfig(ServerConfig):
@@ -315,8 +456,8 @@ def save(self, params: DidSaveTextDocumentParams):
else:
config = SphinxConfig()
- conf_dir = find_conf_dir(
- self.workspace.root_uri, config
+ conf_dir = config.resolve_conf_dir(
+ self.workspace.root_uri
) or pathlib.Path(".")
if str(conf_dir / "conf.py") == filepath:
@@ -383,13 +524,13 @@ def create_sphinx_app(self, options: InitializationOptions) -> Optional[Sphinx]:
self.logger.debug("Workspace root %s", self.workspace.root_uri)
self.logger.debug("Sphinx Config %s", sphinx.dict())
- conf_dir = find_conf_dir(self.workspace.root_uri, sphinx)
+ conf_dir = sphinx.resolve_conf_dir(self.workspace.root_uri)
if conf_dir is None:
raise MissingConfigError()
builder_name = sphinx.builder_name
- src_dir = get_src_dir(self.workspace.root_uri, conf_dir, sphinx)
- build_dir = get_build_dir(self.workspace.root_uri, conf_dir, sphinx)
+ src_dir = sphinx.resolve_src_dir(self.workspace.root_uri, conf_dir)
+ build_dir = sphinx.resolve_build_dir(self.workspace.root_uri, conf_dir)
doctree_dir = build_dir / "doctrees"
build_dir /= builder_name
@@ -821,161 +962,6 @@ def get_intersphinx_targets(
return targets
-def find_conf_dir(root_uri: str, config: SphinxConfig) -> Optional[pathlib.Path]:
- """Attempt to find Sphinx's configuration file within the given workspace."""
-
- root = Uri.to_fs_path(root_uri)
-
- if config.conf_dir:
- return expand_conf_dir(root, config.conf_dir)
-
- ignore_paths = [".tox", "site-packages"]
-
- for candidate in pathlib.Path(root).glob("**/conf.py"):
- # Skip any files that obviously aren't part of the project
- if any(path in str(candidate) for path in ignore_paths):
- continue
-
- return candidate.parent
-
- return None
-
-
-def expand_conf_dir(root_dir: str, conf_dir: str) -> pathlib.Path:
- """Expand the user provided conf_dir into a real path.
-
- Here is where we handle "variables" that can be included in the path, currently
- we support
-
- - ``${workspaceRoot}`` which expands to the workspace root as provided by the
- language client.
- - ``${workspaceFolder}`` alias for ``${workspaceRoot}, placeholder ready for
- multi-root support.
-
- Parameters
- ----------
- root_dir:
- The workspace root path
- conf_dir:
- The user provided path
- """
-
- match = PATH_VAR_PATTERN.match(conf_dir)
- if not match or match.group(1) not in {"workspaceRoot", "workspaceFolder"}:
- return pathlib.Path(conf_dir).expanduser()
-
- conf = pathlib.Path(conf_dir).parts[1:]
- return pathlib.Path(root_dir, *conf).resolve()
-
-
-def get_src_dir(
- root_uri: str, conf_dir: pathlib.Path, config: SphinxConfig
-) -> pathlib.Path:
- """Get the src dir to use based on the given conifg.
-
- By default the src dir will be the same as the conf dir, but this can
- be overriden by the given conifg.
-
- There are a number of "variables" that can be included in the path,
- currently we support
-
- - ``${workspaceRoot}`` which expands to the workspace root as provided
- by the language client.
- - ``${workspaceFolder}`` alias for ``${workspaceRoot}, placeholder ready for
- multi-root support.
- - ``${confDir}`` which expands to the configured config dir.
-
- Parameters
- ----------
- root_uri:
- The workspace root uri
- conf_dir:
- The project's conf dir
- config:
- The user's configuration.
- """
-
- if not config.src_dir:
- return conf_dir
-
- src_dir = config.src_dir
- root_dir = Uri.to_fs_path(root_uri)
-
- match = PATH_VAR_PATTERN.match(src_dir)
- if match and match.group(1) in {"workspaceRoot", "workspaceFolder"}:
- src = pathlib.Path(src_dir).parts[1:]
- return pathlib.Path(root_dir, *src).resolve()
-
- if match and match.group(1) == "confDir":
- src = pathlib.Path(src_dir).parts[1:]
- return pathlib.Path(conf_dir, *src).resolve()
-
- return pathlib.Path(src_dir).expanduser()
-
-
-def get_build_dir(
- root_uri: str, conf_dir: pathlib.Path, config: SphinxConfig
-) -> pathlib.Path:
- """Get the build dir to use based on the given conifg.
-
- If nothing is specified in the given ``config``, this will choose a location within
- the user's cache dir (as determined by `appdirs `).
- The directory name will be a hash derived from the given ``conf_dir`` for the
- project.
-
- Alternatively the user (or least language client) can override this by setting
- either an absolute path, or a path based on the following "variables".
-
- - ``${workspaceRoot}`` which expands to the workspace root as provided
- by the language client.
- - ``${workspaceFolder}`` alias for ``${workspaceRoot}, placeholder ready for
- multi-root support.
- - ``${confDir}`` which expands to the configured config dir.
-
- Parameters
- ----------
- root_uri:
- The workspace root uri
- conf_dir:
- The project's conf dir
- config:
- The user's configuration.
- """
-
- if not config.build_dir:
- # Try to pick a sensible dir based on the project's location
- cache = appdirs.user_cache_dir("esbonio", "swyddfa")
- project = hashlib.md5(str(conf_dir).encode()).hexdigest()
-
- return pathlib.Path(cache) / project
-
- root_dir = Uri.to_fs_path(root_uri)
- match = PATH_VAR_PATTERN.match(config.build_dir)
-
- if match and match.group(1) in {"workspaceRoot", "workspaceFolder"}:
- build = pathlib.Path(config.build_dir).parts[1:]
- return pathlib.Path(root_dir, *build).resolve()
-
- if match and match.group(1) == "confDir":
- build = pathlib.Path(config.build_dir).parts[1:]
- return pathlib.Path(conf_dir, *build).resolve()
-
- # Convert path to/from uri so that any path quirks from windows are
- # automatically handled
- build_uri = Uri.from_fs_path(config.build_dir)
- build_dir = Uri.to_fs_path(build_uri)
-
- # But make sure paths starting with '~' are not corrupted
- if build_dir.startswith("/~"):
- build_dir = build_dir.replace("/~", "~")
-
- # But make sure paths starting with '~' are not corrupted
- if build_dir.startswith("\\~"):
- build_dir = build_dir.replace("\\~", "~")
-
- return pathlib.Path(build_dir).expanduser()
-
-
def exception_to_diagnostic(exc: BaseException):
"""Convert an exception into a diagnostic we can send to the client."""
diff --git a/lib/esbonio/tests/unit_tests/test_sphinx.py b/lib/esbonio/tests/unit_tests/test_sphinx.py
index e10f0afb4..105fba5ef 100644
--- a/lib/esbonio/tests/unit_tests/test_sphinx.py
+++ b/lib/esbonio/tests/unit_tests/test_sphinx.py
@@ -2,9 +2,6 @@
import pytest
-from esbonio.lsp.sphinx import expand_conf_dir
-from esbonio.lsp.sphinx import get_build_dir
-from esbonio.lsp.sphinx import get_src_dir
from esbonio.lsp.sphinx import SphinxConfig
@@ -37,12 +34,13 @@
),
],
)
-def test_expand_conf_dir(setup, expected):
- """Ensure that the ``expand_conf_dir`` function works as expected."""
+def test_resolve_conf_dir(setup, expected):
+ """Ensure that the ``resolve_conf_dir`` function works as expected."""
root_uri, conf_dir = setup
+ config = SphinxConfig(confDir=conf_dir)
- actual = expand_conf_dir(root_uri, conf_dir)
+ actual = config.resolve_conf_dir(root_uri)
assert actual == expected
@@ -115,12 +113,12 @@ def test_expand_conf_dir(setup, expected):
),
],
)
-def test_get_src_dir(setup, expected):
- """Ensure that the ``get_src_dir`` function works as expected."""
+def test_resolve_src_dir(setup, expected):
+ """Ensure that the ``resolve_src_dir`` function works as expected."""
- root_uri, conf_dir, sphinx_config = setup
+ root_uri, conf_dir, config = setup
- actual = get_src_dir(root_uri, conf_dir, sphinx_config)
+ actual = config.resolve_src_dir(root_uri, conf_dir)
assert actual == expected
@@ -193,10 +191,10 @@ def test_get_src_dir(setup, expected):
),
],
)
-def test_get_build_dir(setup, expected):
- """Ensure that the ``get_build_dir`` function works as expected."""
+def test_resolve_build_dir(setup, expected):
+ """Ensure that the ``resolve_build_dir`` function works as expected."""
- root_uri, conf_dir, sphinx_config = setup
+ root_uri, conf_dir, config = setup
- actual = get_build_dir(root_uri, conf_dir, sphinx_config)
+ actual = config.resolve_build_dir(root_uri, conf_dir)
assert actual == expected
From a015f7318774dd5c1285fa96462f6a97867f3a93 Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Sun, 17 Apr 2022 23:37:48 +0100
Subject: [PATCH 06/12] lsp: Add parallel build support
The `SphinxConfig` object is now reponsible for preparing the arguments
used to create the Sphinx application object.
The `sphinx.numJobs` initialization option can be provided by clients to
control the number of parallel jobs used by Sphinx.
---
code/changes/359.enhancement.rst | 1 +
code/package.json | 6 ++
code/src/lsp/client.ts | 36 ++++---
docs/lsp/api-reference.rst | 11 ++
docs/lsp/getting-started.rst | 6 ++
lib/esbonio/changes/359.enhancement.rst | 1 +
lib/esbonio/esbonio/lsp/sphinx/__init__.py | 111 ++++++++++++++-------
lib/esbonio/setup.cfg | 2 +
8 files changed, 123 insertions(+), 51 deletions(-)
create mode 100644 code/changes/359.enhancement.rst
create mode 100644 lib/esbonio/changes/359.enhancement.rst
diff --git a/code/changes/359.enhancement.rst b/code/changes/359.enhancement.rst
new file mode 100644
index 000000000..45963b407
--- /dev/null
+++ b/code/changes/359.enhancement.rst
@@ -0,0 +1 @@
+Added the ``esbonio.sphinx.numJobs`` option (default: ``auto``) which can be used to control the number of parallel jobs used by Sphinx.
diff --git a/code/package.json b/code/package.json
index 3b6ccc98c..fc3c2a92e 100644
--- a/code/package.json
+++ b/code/package.json
@@ -256,6 +256,12 @@
"default": true,
"description": "By default the language server will force a full build of your documentation on startup to help improve the accuracy of some features like diagnostics. Disabling this option can help improve startup time for larger projects at the expense of certain features being less accurate."
},
+ "esbonio.sphinx.numJobs": {
+ "scope": "window",
+ "type": "integer",
+ "default": 0,
+ "markdownDescription": "The number of parallel jobs to use during a Sphinx build.\n\n- A value of `0` is equivalent to passing `-j auto` to a `sphinx-build` command.\n- A value of `1` will disable parallel processing."
+ },
"esbonio.sphinx.srcDir": {
"scope": "window",
"type": "string",
diff --git a/code/src/lsp/client.ts b/code/src/lsp/client.ts
index 603872e49..061ebb178 100644
--- a/code/src/lsp/client.ts
+++ b/code/src/lsp/client.ts
@@ -19,9 +19,14 @@ const DEBUG = process.env.VSCODE_LSP_DEBUG === "true"
export interface SphinxConfig {
/**
- * Sphinx's version number.
+ * The directory where Sphinx's build output should be stored.
*/
- version?: string
+ buildDir?: string
+
+ /**
+ * The name of the builder to use.
+ */
+ builderName?: string
/**
* The directory containing the project's 'conf.py' file.
@@ -29,24 +34,24 @@ export interface SphinxConfig {
confDir?: string
/**
- * The source dir containing the *.rst files for the project.
+ * Flag to force a full build of the documentation on startup.
*/
- srcDir?: string
+ forceFullBuild?: boolean
/**
- * The directory where Sphinx's build output should be stored.
+ * The number of parallel jobs to use
*/
- buildDir?: string
+ numJobs?: number | string
/**
- * The name of the builder to use.
+ * The source dir containing the *.rst files for the project.
*/
- builderName?: string
+ srcDir?: string
/**
- * Flag to force a full build of the documentation on startup.
+ * Sphinx's version number.
*/
- forceFullBuild?: boolean
+ version?: string
}
/**
@@ -289,19 +294,22 @@ export class EsbonioClient {
*/
private getLanguageClientOptions(config: vscode.WorkspaceConfiguration): LanguageClientOptions {
- let cache = this.context.storageUri.path
let buildDir = config.get('sphinx.buildDir')
+ let numJobs = config.get('sphinx.numJobs')
+
if (!buildDir) {
+ let cache = this.context.storageUri.path
buildDir = join(cache, 'sphinx')
}
let initOptions: InitOptions = {
sphinx: {
- srcDir: config.get("sphinx.srcDir"),
- confDir: config.get('sphinx.confDir'),
buildDir: buildDir,
- forceFullBuild: config.get('sphinx.forceFullBuild')
+ confDir: config.get('sphinx.confDir'),
+ forceFullBuild: config.get('sphinx.forceFullBuild'),
+ numJobs: numJobs === 0 ? 'auto' : numJobs,
+ srcDir: config.get("sphinx.srcDir"),
},
server: {
logLevel: config.get('server.logLevel'),
diff --git a/docs/lsp/api-reference.rst b/docs/lsp/api-reference.rst
index 38287b33c..88a28a814 100644
--- a/docs/lsp/api-reference.rst
+++ b/docs/lsp/api-reference.rst
@@ -11,14 +11,25 @@ Language Servers
.. autofunction:: esbonio.lsp.create_language_server
+RstLanguageServer
+^^^^^^^^^^^^^^^^^
+
.. autoclass:: esbonio.lsp.rst.RstLanguageServer
:members:
:show-inheritance:
+SphinxLanguageServer
+^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: esbonio.lsp.sphinx.SphinxConfig
+ :members:
+
.. autoclass:: esbonio.lsp.sphinx.SphinxLanguageServer
:members:
:show-inheritance:
+.. autoclass:: esbonio.lsp.sphinx.MissingConfigError
+
Language Features
-----------------
diff --git a/docs/lsp/getting-started.rst b/docs/lsp/getting-started.rst
index d2e2342d5..1c5a8e335 100644
--- a/docs/lsp/getting-started.rst
+++ b/docs/lsp/getting-started.rst
@@ -208,6 +208,12 @@ Configuration
Flag that indicates if the server should force a full build of the documentation on startup. (Default: ``true``)
+.. confval:: sphinx.numJobs (string or integer)
+
+ Controls the number of parallel jobs used during a Sphinx build.
+
+ The default value of ``"auto"`` will behave the same as passing ``-j auto`` to a ``sphinx-build`` command. Setting this value to ``1`` effectively disables parallel builds.
+
.. confval:: server.logLevel (string)
This can be used to set the level of log messages emitted by the server. This can be set
diff --git a/lib/esbonio/changes/359.enhancement.rst b/lib/esbonio/changes/359.enhancement.rst
new file mode 100644
index 000000000..a7a6786de
--- /dev/null
+++ b/lib/esbonio/changes/359.enhancement.rst
@@ -0,0 +1 @@
+Language clients can now control the number of parallel jobs by providing a ``sphinx.numJobs`` initialization option, which defaults to ``auto``. Clients can disable parallel builds by setting this option to ``1``
diff --git a/lib/esbonio/esbonio/lsp/sphinx/__init__.py b/lib/esbonio/esbonio/lsp/sphinx/__init__.py
index b87253213..bcd79a6b8 100644
--- a/lib/esbonio/esbonio/lsp/sphinx/__init__.py
+++ b/lib/esbonio/esbonio/lsp/sphinx/__init__.py
@@ -1,5 +1,7 @@
import hashlib
+import json
import logging
+import multiprocessing
import pathlib
import platform
import re
@@ -13,6 +15,7 @@
from typing import List
from typing import Optional
from typing import Tuple
+from typing import Union
import appdirs
import pygls.uris as Uri
@@ -37,6 +40,7 @@
from sphinx.util import console
from sphinx.util.logging import SphinxLogRecord
from sphinx.util.logging import WarningLogRecordTranslator
+from typing_extensions import Literal
from esbonio.cli import setup_cli
from esbonio.lsp.rst import LspHandler
@@ -85,7 +89,8 @@ class MissingConfigError(Exception):
class SphinxConfig(BaseModel):
"""Used to represent either the current Sphinx configuration or the config options
- provided by the user at startup."""
+ provided by the user at startup.
+ """
version: Optional[str]
"""Sphinx's version number."""
@@ -106,6 +111,55 @@ class SphinxConfig(BaseModel):
force_full_build: bool = Field(True, alias="forceFullBuild")
"""Flag that can be used to force a full build on startup."""
+ num_jobs: Union[Literal["auto"], int] = Field("auto", alias="numJobs")
+ """The number of jobs to use for parallel builds."""
+
+ @property
+ def parallel(self) -> int:
+ """The parsed value of the ``num_jobs`` field."""
+
+ if self.num_jobs == "auto":
+ return multiprocessing.cpu_count()
+
+ return self.num_jobs
+
+ def get_sphinx_args(self, root_uri: str) -> Dict[str, Any]:
+ """Get the arguments for the Sphinx application object corresponding to this
+ config.
+
+ Parameters
+ ----------
+ root_uri
+ The workspace root uri
+
+ Raises
+ ------
+ MissingConfigError
+ Raised when a valid conf dir cannot be found.
+ """
+
+ conf_dir = self.resolve_conf_dir(root_uri)
+ if conf_dir is None:
+ raise MissingConfigError()
+
+ builder_name = self.builder_name
+ src_dir = self.resolve_src_dir(root_uri, str(conf_dir))
+ build_dir = self.resolve_build_dir(root_uri, str(conf_dir))
+ doctree_dir = build_dir / "doctrees"
+ build_dir /= builder_name
+
+ return {
+ "buildername": builder_name,
+ "confdir": str(conf_dir),
+ "doctreedir": str(doctree_dir),
+ "freshenv": self.force_full_build,
+ "outdir": str(build_dir),
+ "parallel": self.parallel,
+ "srcdir": str(src_dir),
+ "status": None,
+ "warning": None,
+ }
+
def resolve_build_dir(self, root_uri: str, actual_conf_dir: str) -> pathlib.Path:
"""Get the build dir to use based on the user's config.
@@ -118,9 +172,11 @@ def resolve_build_dir(self, root_uri: str, actual_conf_dir: str) -> pathlib.Path
either an absolute path, or a path based on the following "variables".
- ``${workspaceRoot}`` which expands to the workspace root as provided
- by the language client.
- - ``${workspaceFolder}`` alias for ``${workspaceRoot}, placeholder ready for
- multi-root support.
+ by the language client.
+
+ - ``${workspaceFolder}`` alias for ``${workspaceRoot}``, placeholder ready for
+ multi-root support.
+
- ``${confDir}`` which expands to the configured config dir.
Parameters
@@ -176,7 +232,8 @@ def resolve_conf_dir(self, root_uri: str) -> Optional[pathlib.Path]:
- ``${workspaceRoot}`` which expands to the workspace root as provided by the
language client.
- - ``${workspaceFolder}`` alias for ``${workspaceRoot}, placeholder ready for
+
+ - ``${workspaceFolder}`` alias for ``${workspaceRoot}``, placeholder ready for
multi-root support.
Parameters
@@ -216,9 +273,11 @@ def resolve_src_dir(self, root_uri: str, actual_conf_dir: str) -> pathlib.Path:
currently we support
- ``${workspaceRoot}`` which expands to the workspace root as provided
- by the language client.
- - ``${workspaceFolder}`` alias for ``${workspaceRoot}, placeholder ready for
- multi-root support.
+ by the language client.
+
+ - ``${workspaceFolder}`` alias for ``${workspaceRoot}``, placeholder ready for
+ multi-root support.
+
- ``${confDir}`` which expands to the configured config dir.
Parameters
@@ -231,7 +290,7 @@ def resolve_src_dir(self, root_uri: str, actual_conf_dir: str) -> pathlib.Path:
"""
if not self.src_dir:
- return actual_conf_dir
+ return pathlib.Path(actual_conf_dir)
src_dir = self.src_dir
root_dir = Uri.to_fs_path(root_uri)
@@ -521,39 +580,17 @@ def create_sphinx_app(self, options: InitializationOptions) -> Optional[Sphinx]:
sphinx = options.sphinx
server = options.server
- self.logger.debug("Workspace root %s", self.workspace.root_uri)
- self.logger.debug("Sphinx Config %s", sphinx.dict())
+ self.logger.debug("Workspace root '%s'", self.workspace.root_uri)
+ self.logger.debug("User Config %s", json.dumps(sphinx.dict(), indent=2))
- conf_dir = sphinx.resolve_conf_dir(self.workspace.root_uri)
- if conf_dir is None:
- raise MissingConfigError()
-
- builder_name = sphinx.builder_name
- src_dir = sphinx.resolve_src_dir(self.workspace.root_uri, conf_dir)
- build_dir = sphinx.resolve_build_dir(self.workspace.root_uri, conf_dir)
- doctree_dir = build_dir / "doctrees"
- build_dir /= builder_name
-
- self.logger.debug("Config dir %s", conf_dir)
- self.logger.debug("Src dir %s", src_dir)
- self.logger.debug("Build dir %s", build_dir)
- self.logger.debug("Doctree dir %s", doctree_dir)
+ sphinx_args = sphinx.get_sphinx_args(self.workspace.root_uri)
+ self.logger.debug("Sphinx Args %s", json.dumps(sphinx_args, indent=2))
# Disable color escape codes in Sphinx's log messages
console.nocolor()
+ app = Sphinx(**sphinx_args)
- app = Sphinx(
- srcdir=str(src_dir),
- confdir=str(conf_dir),
- outdir=str(build_dir),
- doctreedir=str(doctree_dir),
- buildername=builder_name,
- status=None, # type: ignore
- warning=None, # type: ignore
- freshenv=sphinx.force_full_build,
- )
-
- # This has to happen after app creation otherwise our handler
+ # This has to happen after app creation otherwise our logging handler
# will get cleared by Sphinx's setup.
if not server.hide_sphinx_output:
sphinx_logger = logging.getLogger("sphinx")
diff --git a/lib/esbonio/setup.cfg b/lib/esbonio/setup.cfg
index 04015aff2..555634bcd 100644
--- a/lib/esbonio/setup.cfg
+++ b/lib/esbonio/setup.cfg
@@ -35,6 +35,8 @@ install_requires =
sphinx
pygls>=0.11.0,<0.12.0
pyspellchecker
+ typing-extensions
+
[options.packages.find]
exclude = tests*
From 5ae009fce59af15d8dfa6e591fbd3cdeaf4899ff Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Mon, 18 Apr 2022 12:39:48 +0100
Subject: [PATCH 07/12] lsp: Add `textDocument/documentLink` support to the
server
---
lib/esbonio/esbonio/lsp/__init__.py | 16 ++++++++++++++++
lib/esbonio/esbonio/lsp/rst/__init__.py | 24 +++++++++++++++++++++++-
2 files changed, 39 insertions(+), 1 deletion(-)
diff --git a/lib/esbonio/esbonio/lsp/__init__.py b/lib/esbonio/esbonio/lsp/__init__.py
index daf3cf07b..11260dccd 100644
--- a/lib/esbonio/esbonio/lsp/__init__.py
+++ b/lib/esbonio/esbonio/lsp/__init__.py
@@ -13,6 +13,7 @@
from pygls.lsp.methods import COMPLETION
from pygls.lsp.methods import COMPLETION_ITEM_RESOLVE
from pygls.lsp.methods import DEFINITION
+from pygls.lsp.methods import DOCUMENT_LINK
from pygls.lsp.methods import DOCUMENT_SYMBOL
from pygls.lsp.methods import INITIALIZE
from pygls.lsp.methods import INITIALIZED
@@ -31,6 +32,7 @@
from pygls.lsp.types import DidChangeTextDocumentParams
from pygls.lsp.types import DidOpenTextDocumentParams
from pygls.lsp.types import DidSaveTextDocumentParams
+from pygls.lsp.types import DocumentLinkParams
from pygls.lsp.types import DocumentSymbolParams
from pygls.lsp.types import FileOperationFilter
from pygls.lsp.types import FileOperationPattern
@@ -42,6 +44,7 @@
from .rst import CompletionContext
from .rst import DefinitionContext
+from .rst import DocumentLinkContext
from .rst import LanguageFeature
from .rst import RstLanguageServer
from .rst import SymbolVisitor
@@ -51,6 +54,7 @@
__all__ = [
"CompletionContext",
"DefinitionContext",
+ "DocumentLinkContext",
"LanguageFeature",
"RstLanguageServer",
"create_language_server",
@@ -260,6 +264,18 @@ def on_definition(ls: RstLanguageServer, params: DefinitionParams):
return definitions
+ @server.feature(DOCUMENT_LINK)
+ def on_document_link(ls: RstLanguageServer, params: DocumentLinkParams):
+ uri = params.text_document.uri
+ doc = ls.workspace.get_document(uri)
+ context = DocumentLinkContext(doc=doc, capabilities=ls.client_capabilities)
+
+ links = []
+ for feature in ls._features.values():
+ links += feature.document_link(context) or []
+
+ return links
+
@server.feature(DOCUMENT_SYMBOL)
def on_document_symbol(ls: RstLanguageServer, params: DocumentSymbolParams):
diff --git a/lib/esbonio/esbonio/lsp/rst/__init__.py b/lib/esbonio/esbonio/lsp/rst/__init__.py
index 915c25ec8..41860e4bd 100644
--- a/lib/esbonio/esbonio/lsp/rst/__init__.py
+++ b/lib/esbonio/esbonio/lsp/rst/__init__.py
@@ -28,6 +28,7 @@
from pygls.lsp.types import DeleteFilesParams
from pygls.lsp.types import Diagnostic
from pygls.lsp.types import DidSaveTextDocumentParams
+from pygls.lsp.types import DocumentLink
from pygls.lsp.types import DocumentSymbol
from pygls.lsp.types import InitializedParams
from pygls.lsp.types import InitializeParams
@@ -82,7 +83,6 @@ def __init__(
"""The position at which the completion request was made."""
self._client_capabilities: ClientCapabilities = capabilities
- """The client's capabilities"""
def __repr__(self):
p = f"{self.position.line}:{self.position.character}"
@@ -147,6 +147,24 @@ def supported_tags(self) -> List[CompletionItemTag]:
return capabilities.value_set
+class DocumentLinkContext:
+ """Captures the context within which a document link request has been made."""
+
+ def __init__(self, *, doc: Document, capabilities: ClientCapabilities):
+
+ self.doc = doc
+ """The document within which the document link request was made."""
+
+ self._client_capabilities = capabilities
+
+ @property
+ def tooltip_support(self) -> bool:
+ """Indicates if the client supports tooltips."""
+ return self._client_capabilities.get_capability(
+ "text_document.document_link.tooltip_support", False
+ )
+
+
class DefinitionContext:
"""A class that captures the context within which a definition request has been
made."""
@@ -245,6 +263,10 @@ def definition(self, context: DefinitionContext) -> List[Location]:
"""
return []
+ def document_link(self, context: DocumentLinkContext) -> List[DocumentLink]:
+ """Called whenever a ``textDocument/documentLink`` request is received."""
+ return []
+
class DiagnosticList(collections.UserList):
"""A list type dedicated to holding diagnostics.
From 68e9d3452bb2250a6aef1c45f2734cb7bf42c19e Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Mon, 18 Apr 2022 12:49:46 +0100
Subject: [PATCH 08/12] lsp: Implement `textDocument/documentLink` for role
targets
---
lib/esbonio/esbonio/lsp/roles.py | 106 ++++++++++++++++++++--
lib/esbonio/esbonio/lsp/sphinx/domains.py | 75 ++++++++++++---
2 files changed, 162 insertions(+), 19 deletions(-)
diff --git a/lib/esbonio/esbonio/lsp/roles.py b/lib/esbonio/esbonio/lsp/roles.py
index cc92a021a..8d598be63 100644
--- a/lib/esbonio/esbonio/lsp/roles.py
+++ b/lib/esbonio/esbonio/lsp/roles.py
@@ -6,31 +6,28 @@
from typing import Dict
from typing import List
from typing import Optional
+from typing import Tuple
import pkg_resources
from pygls.lsp.types import CompletionItem
from pygls.lsp.types import CompletionItemKind
+from pygls.lsp.types import DocumentLink
from pygls.lsp.types import Location
from pygls.lsp.types import MarkupContent
from pygls.lsp.types import MarkupKind
from pygls.lsp.types import Position
from pygls.lsp.types import Range
from pygls.lsp.types import TextEdit
+from typing_extensions import Protocol
from esbonio.lsp.directives import DIRECTIVE
from esbonio.lsp.rst import CompletionContext
from esbonio.lsp.rst import DefinitionContext
+from esbonio.lsp.rst import DocumentLinkContext
from esbonio.lsp.rst import LanguageFeature
from esbonio.lsp.rst import RstLanguageServer
from esbonio.lsp.sphinx import SphinxLanguageServer
-try:
- from typing import Protocol
-except ImportError:
- # Protocol is only available in Python 3.8+
- class Protocol: # type: ignore
- ...
-
ROLE = re.compile(
r"""
@@ -146,6 +143,30 @@ def complete_targets(
"""
+class TargetLink(Protocol):
+ """A document link provider for role targets"""
+
+ def resolve_link(
+ self, context: DocumentLinkContext, name: str, domain: Optional[str], label: str
+ ) -> Tuple[Optional[str], Optional[str]]:
+ """Return a link corresponding to the given target.
+
+ Parameters
+ ----------
+ context
+ The document link context
+
+ domain
+ The name (if applicable) of the domain the role is a member of
+
+ name
+ The name of the role to generate completion suggestions for.
+
+ label
+ The label of the target to provide the link for
+ """
+
+
class Roles(LanguageFeature):
"""Role support for the language server."""
@@ -158,14 +179,41 @@ def __init__(self, *args, **kwargs):
self._target_definition_providers: List[TargetDefinition] = []
"""A list of providers that locate the definition for the given role target."""
+ self._target_link_providers: List[TargetLink] = []
+ """A list of providers that resolve document links for role targets."""
+
self._target_completion_providers: List[TargetCompletion] = []
"""A list of providers that give completion suggestions for role target
objects."""
def add_target_definition_provider(self, provider: TargetDefinition) -> None:
+ """Register a :class:`~esbonio.lsp.roles.TargetDefinition` provider.
+
+ Parameters
+ ----------
+ provider
+ The provider to register
+ """
self._target_definition_providers.append(provider)
+ def add_target_link_provider(self, provider: TargetLink) -> None:
+ """Register a :class:`~esbonio.lsp.roles.TargetLink` provider.
+
+ Parameters
+ ----------
+ provider
+ The provider to register
+ """
+ self._target_link_providers.append(provider)
+
def add_target_completion_provider(self, provider: TargetCompletion) -> None:
+ """Register a :class:`~esbonio.lsp.roles.TargetCompletion` provider.
+
+ Parameters
+ ----------
+ provider
+ The provider to register
+ """
self._target_completion_providers.append(provider)
def add_documentation(self, documentation: Dict[str, Dict[str, Any]]) -> None:
@@ -252,6 +300,50 @@ def definition(self, context: DefinitionContext) -> List[Location]:
return definitions
+ def document_link(self, context: DocumentLinkContext) -> List[DocumentLink]:
+
+ links = []
+
+ for line, text in enumerate(context.doc.lines):
+ for match in ROLE.finditer(text):
+ label = match.group("label")
+
+ # Be sure to only match complete roles
+ if not label or not match.group(0).endswith("`"):
+ continue
+
+ domain = match.group("domain")
+ name = match.group("name")
+
+ target = None
+ tooltip = None
+ for provider in self._target_link_providers:
+ target, tooltip = provider.resolve_link(
+ context, name, domain, label
+ )
+ if target:
+ break
+
+ if not target:
+ continue
+
+ idx = match.group(0).index(label)
+ start = match.start() + idx
+ end = start + len(label)
+
+ links.append(
+ DocumentLink(
+ target=target,
+ tooltip=tooltip if context.tooltip_support else None,
+ range=Range(
+ start=Position(line=line, character=start),
+ end=Position(line=line, character=end),
+ ),
+ )
+ )
+
+ return links
+
def complete(self, context: CompletionContext) -> List[CompletionItem]:
"""Generate completion suggestions relevant to the current context.
diff --git a/lib/esbonio/esbonio/lsp/sphinx/domains.py b/lib/esbonio/esbonio/lsp/sphinx/domains.py
index 3f3a8479d..f3405220b 100644
--- a/lib/esbonio/esbonio/lsp/sphinx/domains.py
+++ b/lib/esbonio/esbonio/lsp/sphinx/domains.py
@@ -4,6 +4,7 @@
from typing import List
from typing import Optional
from typing import Set
+from typing import Tuple
import pygls.uris as Uri
from docutils import nodes
@@ -18,6 +19,7 @@
from esbonio.lsp.roles import Roles
from esbonio.lsp.rst import CompletionContext
from esbonio.lsp.rst import DefinitionContext
+from esbonio.lsp.rst import DocumentLinkContext
from esbonio.lsp.rst import RstLanguageServer
from esbonio.lsp.sphinx import SphinxLanguageServer
@@ -76,6 +78,24 @@ def complete_intersphinx_targets(
return items
+ def resolve_link(
+ self, context: DocumentLinkContext, name: str, domain: Optional[str], label: str
+ ) -> Tuple[Optional[str], Optional[str]]:
+ """``textDocument/documentLink`` support"""
+
+ # We can support intersphinx links.
+ if ":" in label:
+ return self.resolve_intersphinx(name, domain, label)
+
+ # We can also support local `:doc:` roles.
+ if not domain and name == "doc":
+ return self.resolve_doc(context.doc, label), None
+
+ # Other roles like :ref: do not make sense as the ``textDocument/documentLink``
+ # api doesn't support specific locations like goto definition does.
+
+ return None, None
+
def find_definitions(
self, context: DefinitionContext, name: str, domain: Optional[str]
) -> List[Location]:
@@ -93,20 +113,13 @@ def find_definitions(
def doc_definition(self, doc: Document, label: str) -> List[Location]:
"""Goto definition implementation for ``:doc:`` targets"""
- if self.rst.app is None:
+ uri = self.resolve_doc(doc, label)
+ if not uri:
return []
- srcdir = self.rst.app.srcdir
- currentdir = pathlib.Path(Uri.to_fs_path(doc.uri)).parent
-
- if label.startswith("/"):
- path = str(pathlib.Path(srcdir, label[1:] + ".rst"))
- else:
- path = str(pathlib.Path(currentdir, label + ".rst"))
-
return [
Location(
- uri=Uri.from_fs_path(path),
+ uri=uri,
range=Range(
start=Position(line=0, character=0),
end=Position(line=1, character=0),
@@ -156,6 +169,42 @@ def ref_definition(self, label: str) -> List[Location]:
)
]
+ def resolve_doc(self, doc: Document, label: str) -> Optional[str]:
+
+ if self.rst.app is None:
+ return None
+
+ srcdir = self.rst.app.srcdir
+ currentdir = pathlib.Path(Uri.to_fs_path(doc.uri)).parent
+
+ if label.startswith("/"):
+ path = str(pathlib.Path(srcdir, label[1:] + ".rst"))
+ else:
+ path = str(pathlib.Path(currentdir, label + ".rst"))
+
+ return Uri.from_fs_path(path)
+
+ def resolve_intersphinx(
+ self, name: str, domain: Optional[str], label: str
+ ) -> Tuple[Optional[str], Optional[str]]:
+ """Resolve an intersphinx reference to a URL"""
+
+ if not self.rst.app:
+ return None, None
+
+ project, label = label.split(":")
+ targets = self.rst.get_intersphinx_targets(project, name, domain or "")
+
+ for _, items in targets.items():
+ if label in items:
+ source, version, url, display = items[label]
+ name = label if display == "-" else display
+ tooltip = f"{name} - {source} v{version}"
+
+ return url, tooltip
+
+ return None, None
+
def find_docname_for_label(
self, label: str, domain: Domain, types: Optional[Set[str]] = None
) -> Optional[str]:
@@ -248,5 +297,7 @@ def esbonio_setup(rst: RstLanguageServer):
roles = rst.get_feature("esbonio.lsp.roles.Roles")
if roles:
- typing.cast(Roles, roles).add_target_definition_provider(domains)
- typing.cast(Roles, roles).add_target_completion_provider(domains)
+ roles = typing.cast(Roles, roles) # let's keep mypy happy
+ roles.add_target_definition_provider(domains)
+ roles.add_target_completion_provider(domains)
+ roles.add_target_link_provider(domains)
From be44662a744245e1240ce9133e24edd669ff2306 Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Mon, 18 Apr 2022 15:10:27 +0100
Subject: [PATCH 09/12] lsp: Implement `textDocument/documentLink` for
directive arguments
---
lib/esbonio/changes/294.feature.rst | 5 +
lib/esbonio/esbonio/lsp/directives.py | 93 ++++++++++++++++++-
lib/esbonio/esbonio/lsp/sphinx/images.py | 49 +++++++---
lib/esbonio/esbonio/lsp/sphinx/includes.py | 49 +++++++---
.../tests/sphinx-default/test_sd_sphinx.py | 60 ++++++++++++
.../sphinx-default/workspace/definitions.rst | 2 +
.../tests/sphinx-extensions/test_se_sphinx.py | 55 +++++++++++
.../workspace/sphinx-extensions/conf.py | 2 +-
.../sphinx-extensions/definitions.rst | 10 ++
9 files changed, 296 insertions(+), 29 deletions(-)
create mode 100644 lib/esbonio/changes/294.feature.rst
create mode 100644 lib/esbonio/tests/sphinx-extensions/test_se_sphinx.py
create mode 100644 lib/esbonio/tests/sphinx-extensions/workspace/sphinx-extensions/definitions.rst
diff --git a/lib/esbonio/changes/294.feature.rst b/lib/esbonio/changes/294.feature.rst
new file mode 100644
index 000000000..d7dbd2ca6
--- /dev/null
+++ b/lib/esbonio/changes/294.feature.rst
@@ -0,0 +1,5 @@
+Add ``textDocument/documentLink`` support.
+
+The server supports resolving links for role targets with initial support for intersphinx references and local ``:doc:`` references.
+
+The server also supports resolving links for directive arguments with initial support for ``.. image::``, ``.. figure::``, ``.. include::`` and ``.. literalinclude::`` directives.
diff --git a/lib/esbonio/esbonio/lsp/directives.py b/lib/esbonio/esbonio/lsp/directives.py
index 84162d251..ef3c79d75 100644
--- a/lib/esbonio/esbonio/lsp/directives.py
+++ b/lib/esbonio/esbonio/lsp/directives.py
@@ -6,10 +6,12 @@
from typing import Dict
from typing import List
from typing import Optional
+from typing import Tuple
import pkg_resources
from pygls.lsp.types import CompletionItem
from pygls.lsp.types import CompletionItemKind
+from pygls.lsp.types import DocumentLink
from pygls.lsp.types import InsertTextFormat
from pygls.lsp.types import Location
from pygls.lsp.types import MarkupContent
@@ -20,6 +22,7 @@
from esbonio.lsp import CompletionContext
from esbonio.lsp import DefinitionContext
+from esbonio.lsp import DocumentLinkContext
from esbonio.lsp import LanguageFeature
from esbonio.lsp import RstLanguageServer
from esbonio.lsp.sphinx import SphinxLanguageServer
@@ -131,6 +134,31 @@ def find_definitions(
"""
+class ArgumentLink(Protocol):
+ """A document link resolver for directive arguments."""
+
+ def resolve_link(
+ self,
+ context: DocumentLinkContext,
+ directive: str,
+ domain: Optional[str],
+ argument: str,
+ ) -> Tuple[Optional[str], Optional[str]]:
+ """Resolve a document link request for the given argument.
+
+ Parameters
+ ----------
+ context:
+ The context of the document link request.
+ directive:
+ The name of the directive the argument is associated with.
+ domain:
+ The name of the domain the directive belongs to, if applicable.
+ argument:
+ The argument to resolve the link for.
+ """
+
+
class Directives(LanguageFeature):
"""Directive support for the language server."""
@@ -145,7 +173,10 @@ def __init__(self, *args, **kwargs):
arguments."""
self._argument_definition_providers: Dict[str, ArgumentDefinition] = {}
- """A dictionary of providers that give completion suggestions for directive
+ """A dictionary of providers that locate definitions for directive arguments."""
+
+ self._argument_link_providers: Dict[str, ArgumentLink] = {}
+ """A dictionary of providers that resolve document links for directive
arguments."""
def add_argument_completion_provider(self, provider: ArgumentCompletion) -> None:
@@ -160,7 +191,7 @@ def add_argument_completion_provider(self, provider: ArgumentCompletion) -> None
self._argument_completion_providers[key] = provider
def add_argument_definition_provider(self, provider: ArgumentDefinition) -> None:
- """Register an :class:`~esbonio.lsp.directives.ArgumentCompletion` provider.
+ """Register an :class:`~esbonio.lsp.directives.ArgumentDefinition` provider.
Parameters
----------
@@ -170,6 +201,17 @@ def add_argument_definition_provider(self, provider: ArgumentDefinition) -> None
key = f"{provider.__module__}.{provider.__class__.__name__}"
self._argument_definition_providers[key] = provider
+ def add_argument_link_provider(self, provider: ArgumentLink) -> None:
+ """Register an :class:`~esbonio.lsp.directives.ArgumentLink` provider.
+
+ Parameters
+ ----------
+ provider:
+ The provider to register.
+ """
+ key = f"{provider.__module__}.{provider.__class__.__name__}"
+ self._argument_link_providers[key] = provider
+
def add_documentation(self, documentation: Dict[str, Dict[str, Any]]) -> None:
"""Register directive documentation.
@@ -493,6 +535,48 @@ def find_argument_definition(
return definitions
+ def document_link(self, context: DocumentLinkContext) -> List[DocumentLink]:
+ links = []
+
+ for line, text in enumerate(context.doc.lines):
+ for match in DIRECTIVE.finditer(text):
+
+ argument = match.group("argument")
+ if not argument:
+ continue
+
+ domain = match.group("domain")
+ name = match.group("name")
+
+ target = None
+ tooltip = None
+ for provider in self._argument_link_providers.values():
+ target, tooltip = provider.resolve_link(
+ context, name, domain, argument
+ )
+ if target:
+ break
+
+ if not target:
+ continue
+
+ idx = match.group(0).index(argument)
+ start = match.start() + idx
+ end = start + len(argument)
+
+ links.append(
+ DocumentLink(
+ target=target,
+ tooltip=tooltip if context.tooltip_support else None,
+ range=Range(
+ start=Position(line=line, character=start),
+ end=Position(line=line, character=end),
+ ),
+ )
+ )
+
+ return links
+
def get_surrounding_directive(
self, context: CompletionContext
) -> Optional["re.Match"]:
@@ -552,9 +636,10 @@ def get_documentation(
Parameters
----------
- label:
+ label
The name of the directive, as the user would type in an reStructuredText file.
- implementation:
+
+ implementation
The full dotted name of the directive's implementation.
"""
diff --git a/lib/esbonio/esbonio/lsp/sphinx/images.py b/lib/esbonio/esbonio/lsp/sphinx/images.py
index 8732ae956..eb954540c 100644
--- a/lib/esbonio/esbonio/lsp/sphinx/images.py
+++ b/lib/esbonio/esbonio/lsp/sphinx/images.py
@@ -3,16 +3,19 @@
import typing
from typing import List
from typing import Optional
+from typing import Tuple
import pygls.uris as Uri
from pygls.lsp.types import CompletionItem
from pygls.lsp.types import Location
from pygls.lsp.types import Position
from pygls.lsp.types import Range
+from pygls.workspace import Document
from esbonio.lsp.directives import Directives
from esbonio.lsp.rst import CompletionContext
from esbonio.lsp.rst import DefinitionContext
+from esbonio.lsp.rst import DocumentLinkContext
from esbonio.lsp.rst import RstLanguageServer
from esbonio.lsp.sphinx import SphinxLanguageServer
from esbonio.lsp.util.filepaths import complete_sphinx_filepaths
@@ -52,9 +55,38 @@ def find_definitions(
if domain or directive not in {"figure", "image"}:
return []
+ uri = self.resolve_path(context.doc, argument)
+ if not uri:
+ return []
+
+ return [
+ Location(
+ uri=uri,
+ range=Range(
+ start=Position(line=0, character=0),
+ end=Position(line=1, character=0),
+ ),
+ )
+ ]
+
+ def resolve_link(
+ self,
+ context: DocumentLinkContext,
+ directive: str,
+ domain: Optional[str],
+ argument: str,
+ ) -> Tuple[Optional[str], Optional[str]]:
+
+ if domain or directive not in {"figure", "image"}:
+ return None, None
+
+ return self.resolve_path(context.doc, argument), None
+
+ def resolve_path(self, doc: Document, argument: str) -> Optional[str]:
+
if argument.startswith("/"):
if not self.rst.app:
- return []
+ return None
basedir = pathlib.Path(self.rst.app.srcdir)
@@ -63,21 +95,13 @@ def find_definitions(
argument = argument[1:]
else:
- basedir = pathlib.Path(Uri.to_fs_path(context.doc.uri)).parent
+ basedir = pathlib.Path(Uri.to_fs_path(doc.uri)).parent
fpath = (basedir / argument).resolve()
if not fpath.exists():
- return []
+ return None
- return [
- Location(
- uri=Uri.from_fs_path(str(fpath)),
- range=Range(
- start=Position(line=0, character=0),
- end=Position(line=1, character=0),
- ),
- )
- ]
+ return Uri.from_fs_path(str(fpath))
def esbonio_setup(rst: RstLanguageServer):
@@ -94,3 +118,4 @@ def esbonio_setup(rst: RstLanguageServer):
images = Images(rst)
directives.add_argument_definition_provider(images)
directives.add_argument_completion_provider(images)
+ directives.add_argument_link_provider(images)
diff --git a/lib/esbonio/esbonio/lsp/sphinx/includes.py b/lib/esbonio/esbonio/lsp/sphinx/includes.py
index dc2153454..feb3c7ae9 100644
--- a/lib/esbonio/esbonio/lsp/sphinx/includes.py
+++ b/lib/esbonio/esbonio/lsp/sphinx/includes.py
@@ -3,16 +3,19 @@
import typing
from typing import List
from typing import Optional
+from typing import Tuple
import pygls.uris as Uri
from pygls.lsp.types import CompletionItem
from pygls.lsp.types import Location
from pygls.lsp.types import Position
from pygls.lsp.types import Range
+from pygls.workspace import Document
from esbonio.lsp.directives import Directives
from esbonio.lsp.rst import CompletionContext
from esbonio.lsp.rst import DefinitionContext
+from esbonio.lsp.rst import DocumentLinkContext
from esbonio.lsp.rst import RstLanguageServer
from esbonio.lsp.sphinx import SphinxLanguageServer
from esbonio.lsp.util.filepaths import complete_sphinx_filepaths
@@ -52,9 +55,38 @@ def find_definitions(
if domain or directive not in {"literalinclude", "include"}:
return []
+ uri = self.resolve_path(context.doc, argument)
+ if not uri:
+ return []
+
+ return [
+ Location(
+ uri=uri,
+ range=Range(
+ start=Position(line=0, character=0),
+ end=Position(line=1, character=0),
+ ),
+ )
+ ]
+
+ def resolve_link(
+ self,
+ context: DocumentLinkContext,
+ directive: str,
+ domain: Optional[str],
+ argument: str,
+ ) -> Tuple[Optional[str], Optional[str]]:
+
+ if domain or directive not in {"literalinclude", "include"}:
+ return None, None
+
+ return self.resolve_path(context.doc, argument), None
+
+ def resolve_path(self, doc: Document, argument: str) -> Optional[str]:
+
if argument.startswith("/"):
if not self.rst.app:
- return []
+ return None
basedir = pathlib.Path(self.rst.app.srcdir)
@@ -63,21 +95,13 @@ def find_definitions(
argument = argument[1:]
else:
- basedir = pathlib.Path(Uri.to_fs_path(context.doc.uri)).parent
+ basedir = pathlib.Path(Uri.to_fs_path(doc.uri)).parent
fpath = (basedir / argument).resolve()
if not fpath.exists():
- return []
+ return None
- return [
- Location(
- uri=Uri.from_fs_path(str(fpath)),
- range=Range(
- start=Position(line=0, character=0),
- end=Position(line=1, character=0),
- ),
- )
- ]
+ return Uri.from_fs_path(str(fpath))
def esbonio_setup(rst: RstLanguageServer):
@@ -93,3 +117,4 @@ def esbonio_setup(rst: RstLanguageServer):
includes = Includes(rst)
directives.add_argument_definition_provider(includes)
directives.add_argument_completion_provider(includes)
+ directives.add_argument_link_provider(includes)
diff --git a/lib/esbonio/tests/sphinx-default/test_sd_sphinx.py b/lib/esbonio/tests/sphinx-default/test_sd_sphinx.py
index f9e720e9f..7973e835f 100644
--- a/lib/esbonio/tests/sphinx-default/test_sd_sphinx.py
+++ b/lib/esbonio/tests/sphinx-default/test_sd_sphinx.py
@@ -9,9 +9,12 @@
from pygls import IS_WIN
from pygls.lsp.types import Diagnostic
from pygls.lsp.types import DiagnosticSeverity
+from pygls.lsp.types import DocumentLink
from pygls.lsp.types import MessageType
from pygls.lsp.types import Position
from pygls.lsp.types import Range
+from pytest_lsp import check
+from pytest_lsp import Client
from pytest_lsp import ClientServerConfig
from pytest_lsp import make_client_server
from pytest_lsp import make_test_client
@@ -37,6 +40,63 @@ def _(*args, **kwargs):
return client
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "uri,expected",
+ [
+ (
+ "/definitions.rst",
+ [
+ DocumentLink(
+ target="${ROOT}/theorems/pythagoras.rst",
+ range=Range(
+ start=Position(line=21, character=20),
+ end=Position(line=21, character=44),
+ ),
+ ),
+ DocumentLink(
+ target="${ROOT}/index.rst",
+ range=Range(
+ start=Position(line=23, character=20),
+ end=Position(line=23, character=42),
+ ),
+ ),
+ DocumentLink(
+ target="${ROOT}/_static/vscode-screenshot.png",
+ range=Range(
+ start=Position(line=25, character=11),
+ end=Position(line=25, character=41),
+ ),
+ ),
+ DocumentLink(
+ target="${ROOT}/glossary.rst",
+ range=Range(
+ start=Position(line=31, character=14),
+ end=Position(line=31, character=23),
+ ),
+ ),
+ ],
+ )
+ ],
+)
+async def test_document_links(client: Client, uri: str, expected: List[DocumentLink]):
+ """Ensure that we handle ``textDocument/documentLink`` requests correctly."""
+
+ test_uri = client.root_uri + uri
+ links = await client.document_link_request(test_uri)
+
+ assert len(links) == len(expected)
+
+ for expected, actual in zip(expected, links):
+ assert expected.range == actual.range
+
+ target = expected.target.replace("${ROOT}", client.root_uri)
+ assert target == actual.target
+ assert expected.tooltip == actual.tooltip
+
+ check.document_links(client, links)
+
+
@pytest.mark.asyncio
@pytest.mark.timeout(10)
@pytest.mark.parametrize(
diff --git a/lib/esbonio/tests/sphinx-default/workspace/definitions.rst b/lib/esbonio/tests/sphinx-default/workspace/definitions.rst
index 133d917de..024eadaa2 100644
--- a/lib/esbonio/tests/sphinx-default/workspace/definitions.rst
+++ b/lib/esbonio/tests/sphinx-default/workspace/definitions.rst
@@ -28,3 +28,5 @@ Some literal includes now
.. figure:: /_static/bad.png
This line refers to :ref:`setup-label`
+
+See the :doc:`/glossary` for details
diff --git a/lib/esbonio/tests/sphinx-extensions/test_se_sphinx.py b/lib/esbonio/tests/sphinx-extensions/test_se_sphinx.py
new file mode 100644
index 000000000..3e49ad87b
--- /dev/null
+++ b/lib/esbonio/tests/sphinx-extensions/test_se_sphinx.py
@@ -0,0 +1,55 @@
+from typing import List
+
+import pytest
+from pygls.lsp.types import DocumentLink
+from pygls.lsp.types import Position
+from pygls.lsp.types import Range
+from pytest_lsp import check
+from pytest_lsp import Client
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "uri,expected",
+ [
+ (
+ "/sphinx-extensions/definitions.rst",
+ [
+ DocumentLink(
+ target="https://docs.python.org/3.9/howto/logging.html#logging-basic-tutorial",
+ range=Range(
+ start=Position(line=5, character=29),
+ end=Position(line=5, character=58),
+ ),
+ ),
+ DocumentLink(
+ target="https://docs.python.org/3.9/library/logging.html#logging.Filter",
+ range=Range(
+ start=Position(line=7, character=19),
+ end=Position(line=7, character=40),
+ ),
+ ),
+ DocumentLink(
+ target="https://docs.python.org/3.9/howto/logging-cookbook.html",
+ range=Range(
+ start=Position(line=9, character=18),
+ end=Position(line=9, character=47),
+ ),
+ ),
+ ],
+ )
+ ],
+)
+async def test_document_links(client: Client, uri: str, expected: List[DocumentLink]):
+ """Ensure that we handle ``textDocument/documentLink`` requests correctly."""
+
+ test_uri = client.root_uri + uri
+ links = await client.document_link_request(test_uri)
+
+ assert len(links) == len(expected)
+
+ for expected, actual in zip(expected, links):
+ assert expected.range == actual.range
+ assert expected.target == actual.target
+
+ check.document_links(client, links)
diff --git a/lib/esbonio/tests/sphinx-extensions/workspace/sphinx-extensions/conf.py b/lib/esbonio/tests/sphinx-extensions/workspace/sphinx-extensions/conf.py
index 34f0fc5bc..83a08c54d 100644
--- a/lib/esbonio/tests/sphinx-extensions/workspace/sphinx-extensions/conf.py
+++ b/lib/esbonio/tests/sphinx-extensions/workspace/sphinx-extensions/conf.py
@@ -28,7 +28,7 @@
extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx"]
intersphinx_mapping = {
- "python": ("https://docs.python.org/3", None),
+ "python": ("https://docs.python.org/3.9/", None),
"sphinx": ("https://www.sphinx-doc.org/en/master", None),
}
diff --git a/lib/esbonio/tests/sphinx-extensions/workspace/sphinx-extensions/definitions.rst b/lib/esbonio/tests/sphinx-extensions/workspace/sphinx-extensions/definitions.rst
new file mode 100644
index 000000000..75656ced1
--- /dev/null
+++ b/lib/esbonio/tests/sphinx-extensions/workspace/sphinx-extensions/definitions.rst
@@ -0,0 +1,10 @@
+Definitions
+===========
+
+This file contains a number of test cases for us to test goto definition/document link etc on.
+
+Here is a reference to :ref:`python:logging-basic-tutorial`
+
+One for :py:class:`python:logging.Filter`
+
+Another for :doc:`python:howto/logging-cookbook`
From a97d4d153e83f7e26f648d479898a9f4a756bf83 Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Mon, 18 Apr 2022 15:23:35 +0100
Subject: [PATCH 10/12] docs: Update documentation
---
README.md | 71 +++++++++++++----------
docs/index.rst | 8 +++
docs/lsp/api-reference.rst | 22 +++++--
docs/lsp/extending.rst | 2 +-
resources/images/document-links-demo.png | Bin 0 -> 37800 bytes
5 files changed, 65 insertions(+), 38 deletions(-)
create mode 100644 resources/images/document-links-demo.png
diff --git a/README.md b/README.md
index 582326b9f..8ffd9d202 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,42 @@
Esbonio aims to make it easier to work with [reStructuredText](https://docutils.sourceforge.io/rst.html) tools such as [Sphinx](https://www.sphinx-doc.org/en/master/) by providing a [Language Server](https://langserver.org/) to enhance your editing experience.
The Esbonio project is made up from a number of sub-projects
+## `lib/esbonio/` - A Language Server for Sphinx projects.
+[![PyPI](https://img.shields.io/pypi/v/esbonio?style=flat-square)![PyPI - Downloads](https://img.shields.io/pypi/dm/esbonio?style=flat-square)](https://pypistats.org/packages/esbonio)[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/swyddfa/esbonio/blob/develop/lib/esbonio/LICENSE)
+
+The language server is still in early development, but already provides the following features.
+
+**Completion**
+
+
+
+
+
+**Definitions**
+
+
+
+
+
+**Diagnostics**
+
+
+
+
+
+**Document Links**
+
+
+
+
+
+
+**Document Symbols**
+
+
+
+
+
## `code/` - A VSCode extension for editing Sphinx projects
[![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/swyddfa.esbonio?style=flat-square)![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/swyddfa.esbonio?style=flat-square)![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/swyddfa.esbonio?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=swyddfa.esbonio)[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/swyddfa/esbonio/blob/develop/code/LICENSE)
@@ -17,9 +53,9 @@ The Esbonio project is made up from a number of sub-projects
-Most of the extension's functionality is provided through the `esbonio` language server (see below.)
+This extension is purely focused on bringing the `esbonio` language server into VSCode to help with development and testing new ideas.
-### What about the reStructuredText Extension?
+### You're probably looking for the reStructuredText Extension
You may already be familiar with the [reStructuredText](https://marketplace.visualstudio.com/items?itemName=lextudio.restructuredtext) extension which, as of [v171.0.0](https://github.com/vscode-restructuredtext/vscode-restructuredtext/releases/tag/171.0.0) now also integrates the `esbonio` language server into VSCode.
It also integrates other tools such as the linters [`doc8`](https://pypi.org/project/doc8/) and [`rstcheck`](https://pypi.org/project/rstcheck/) and provides additional editor functionality making it easier to work with reStructuredText in general.
@@ -35,41 +71,12 @@ Finally, the Esbonio extension serves as an up to date reference for projects th
Try the reStructuredText extension if
- You need an extension compatible with older versions of VSCode
-- You are interested in additional features beyond what is provided by the language server
+- You are looking for a more rounded editing experience.
Try the Esbonio extension if
- You want to make use of the newer features available in recent VSCode versions
- You are only interested in the features provided by the language server
-## `lib/esbonio/` - A Language Server for Sphinx projects.
-[![PyPI](https://img.shields.io/pypi/v/esbonio?style=flat-square)![PyPI - Downloads](https://img.shields.io/pypi/dm/esbonio?style=flat-square)](https://pypistats.org/packages/esbonio)[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/swyddfa/esbonio/blob/develop/lib/esbonio/LICENSE)
-
-The language server is still in early development, but already provides the following features.
-
-**Completion**
-
-
-
-
-
-**Definitions**
-
-
-
-
-
-**Diagnostics**
-
-
-
-
-
-**Document Symbols**
-
-
-
-
-
## `lib/esbonio-extensions/` - A collection of Sphinx extensions
[![PyPI](https://img.shields.io/pypi/v/esbonio-extensions?style=flat-square)![PyPI - Downloads](https://img.shields.io/pypi/dm/esbonio-extensions?style=flat-square)](https://pypistats.org/packages/esbonio-extensions)[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/swyddfa/esbonio/blob/develop/lib/esbonio-extensions/LICENSE)
diff --git a/docs/index.rst b/docs/index.rst
index cca0ad099..b620d352b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -41,6 +41,14 @@ Here is a quick summary of the features implemented by the language server.
:align: center
:target: /_images/diagnostic-sphinx-errors-demo.png
+ .. collection-item:: Document Links
+
+ The language server implements :lsp:`textDocument/documentLink` to make references to other files "Ctrl + Clickable"
+
+ .. figure:: ../resources/images/document-links-demo.png
+ :align: center
+ :target: /_images/document-links-demo.png
+
.. collection-item:: Document Symbols
The language server implements :lsp:`textDocument/documentSymbol` which
diff --git a/docs/lsp/api-reference.rst b/docs/lsp/api-reference.rst
index 88a28a814..c9d1b617e 100644
--- a/docs/lsp/api-reference.rst
+++ b/docs/lsp/api-reference.rst
@@ -33,13 +33,16 @@ SphinxLanguageServer
Language Features
-----------------
-.. autoclass:: esbonio.lsp.rst.LanguageFeature
+.. autoclass:: esbonio.lsp.LanguageFeature
:members:
-.. autoclass:: esbonio.lsp.rst.CompletionContext
+.. autoclass:: esbonio.lsp.CompletionContext
:members:
-.. autoclass:: esbonio.lsp.rst.DefinitionContext
+.. autoclass:: esbonio.lsp.DefinitionContext
+ :members:
+
+.. autoclass:: esbonio.lsp.DocumentLinkContext
:members:
Directives
@@ -56,8 +59,14 @@ Directives
.. autoclass:: ArgumentCompletion
:members:
+.. autoclass:: ArgumentDefinition
+ :members:
+
+.. autoclass:: ArgumentLink
+ :members:
+
.. autoclass:: Directives
- :members: add_argument_completion_provider, add_documentation
+ :members: add_argument_completion_provider, add_argument_definition_provider, add_argument_link_provider, add_documentation
Roles
^^^^^
@@ -71,7 +80,7 @@ Roles
:annotation: = re.compile(...)
.. autoclass:: Roles
- :members: add_documentation, add_target_definition_provider, add_target_completion_provider
+ :members: add_documentation, add_target_definition_provider, add_target_completion_provider, add_target_link_provider
.. autoclass:: TargetCompletion
:members:
@@ -79,6 +88,9 @@ Roles
.. autoclass:: TargetDefinition
:members:
+.. autoclass:: TargetLink
+ :members:
+
Testing
-------
diff --git a/docs/lsp/extending.rst b/docs/lsp/extending.rst
index aa9abcc7e..32e9974b7 100644
--- a/docs/lsp/extending.rst
+++ b/docs/lsp/extending.rst
@@ -32,7 +32,7 @@ Architecture
Language Feature
Language features are subclasses of :class:`~esbonio.lsp.rst.LanguageFeature`.
- They are typically based on a single aspect of reStructuedText (e.g. :class:`~esbonio.lsp.roles.Roles`) or Sphinx (e.g. :class:`` responsible for providing
+ They are typically based on a single aspect of reStructuredText (e.g. :class:`~esbonio.lsp.roles.Roles`) or Sphinx (e.g. :class:`` responsible for providing
Language Features (where it makes sense) should be server agnostic, that way the same features can be reused across different envrionments.
diff --git a/resources/images/document-links-demo.png b/resources/images/document-links-demo.png
new file mode 100644
index 0000000000000000000000000000000000000000..d6b39ac425bb6518d7475652dea65e25a42920e3
GIT binary patch
literal 37800
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
literal 0
HcmV?d00001
From fb39a0000a8103ee9977f2cd37a6dab42468baab Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Mon, 18 Apr 2022 18:21:16 +0100
Subject: [PATCH 11/12] lsp: Only return `:doc:` definitions if target exists
---
lib/esbonio/changes/369.fix.rst | 1 +
lib/esbonio/esbonio/lsp/sphinx/domains.py | 9 ++++++---
.../tests/sphinx-default/test_sd_sphinx_domains.py | 4 ++--
3 files changed, 9 insertions(+), 5 deletions(-)
create mode 100644 lib/esbonio/changes/369.fix.rst
diff --git a/lib/esbonio/changes/369.fix.rst b/lib/esbonio/changes/369.fix.rst
new file mode 100644
index 000000000..ad846f780
--- /dev/null
+++ b/lib/esbonio/changes/369.fix.rst
@@ -0,0 +1 @@
+Goto definition for ``:doc:`` targets will now only return a result if the referenced document actually exists.
diff --git a/lib/esbonio/esbonio/lsp/sphinx/domains.py b/lib/esbonio/esbonio/lsp/sphinx/domains.py
index f3405220b..9e2e21917 100644
--- a/lib/esbonio/esbonio/lsp/sphinx/domains.py
+++ b/lib/esbonio/esbonio/lsp/sphinx/domains.py
@@ -178,11 +178,14 @@ def resolve_doc(self, doc: Document, label: str) -> Optional[str]:
currentdir = pathlib.Path(Uri.to_fs_path(doc.uri)).parent
if label.startswith("/"):
- path = str(pathlib.Path(srcdir, label[1:] + ".rst"))
+ path = pathlib.Path(srcdir, label[1:] + ".rst")
else:
- path = str(pathlib.Path(currentdir, label + ".rst"))
+ path = pathlib.Path(currentdir, label + ".rst")
- return Uri.from_fs_path(path)
+ if not path.exists():
+ return None
+
+ return Uri.from_fs_path(str(path))
def resolve_intersphinx(
self, name: str, domain: Optional[str], label: str
diff --git a/lib/esbonio/tests/sphinx-default/test_sd_sphinx_domains.py b/lib/esbonio/tests/sphinx-default/test_sd_sphinx_domains.py
index a435feb3b..268e8842d 100644
--- a/lib/esbonio/tests/sphinx-default/test_sd_sphinx_domains.py
+++ b/lib/esbonio/tests/sphinx-default/test_sd_sphinx_domains.py
@@ -263,9 +263,9 @@ async def test_role_target_completions(client: Client, text: str, setup):
),
(
"definitions.rst",
- Position(line=13, character=36),
+ Position(line=31, character=20),
Location(
- uri="changelog.rst",
+ uri="glossary.rst",
range=Range(
start=Position(line=0, character=0),
end=Position(line=1, character=0),
From 5359e462148dccf73338416333e58ae94f57445b Mon Sep 17 00:00:00 2001
From: Alex Carney
Date: Mon, 18 Apr 2022 20:27:44 +0100
Subject: [PATCH 12/12] code: Bump required esbonio version
---
code/src/constants.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/code/src/constants.ts b/code/src/constants.ts
index 0912c3a92..4d5980868 100644
--- a/code/src/constants.ts
+++ b/code/src/constants.ts
@@ -3,7 +3,7 @@ export const PYTHON_EXTENSION = "ms-python.python"
export namespace Server {
export const REQUIRED_PYTHON = "3.6.0"
- export const REQUIRED_VERSION = "0.10.2"
+ export const REQUIRED_VERSION = "0.11.0"
}
export namespace Commands {