diff --git a/.gitignore b/.gitignore index 7d5cb348..e86da6bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .coverage .env +.ipynb_checkpoints .tox .vscode-test @@ -14,4 +15,4 @@ node_modules dist .changes.html -CHANGELOG.md \ No newline at end of file +CHANGELOG.md diff --git a/docs/conf.py b/docs/conf.py index 6838b37d..a847d81e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,12 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["esbonio.tutorial"] +extensions = ["sphinx.ext.intersphinx", "esbonio.tutorial"] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master", None), +} # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/index.rst b/docs/index.rst index 5d19bcec..8db15631 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,7 +17,8 @@ Welcome to the documentation :hidden: :maxdepth: 2 - lsp/* + lsp/features + lsp/contributing changelog .. toctree:: @@ -26,4 +27,4 @@ Welcome to the documentation :hidden: :caption: VSCode Extension - code/* + code/* \ No newline at end of file diff --git a/docs/lsp/features.rst b/docs/lsp/features.rst new file mode 100644 index 00000000..43e79e51 --- /dev/null +++ b/docs/lsp/features.rst @@ -0,0 +1,105 @@ +Features +======== + +This page contains a quick overview of the features offered by the Language +Server + +Completion +---------- + +The Language Server can offer auto complete suggestions in a variety of contexts + +Directives +^^^^^^^^^^ + +.. figure:: ../../resources/images/complete-directive-demo.gif + :align: center + + Completing directive names + +.. note:: + + Currently the Language Server makes a hardcoded assumption that your + ``primary_domain`` is set to ``python`` and has no knowledge that other + domains exist. + + Support for additional domains will come in a future release + +Directive Options +^^^^^^^^^^^^^^^^^ + +.. figure:: ../../resources/images/complete-directive-options-demo.gif + :align: center + + Completing a directive's options + +Roles +^^^^^ + +.. figure:: ../../resources/images/complete-role-demo.gif + :align: center + + Completing role names + +.. note:: + + Currently the Language Server makes a hardcoded assumption that your + ``primary_domain`` is set to ``python`` and has no knowledge that other + domains exist. + + Support for additional domains will come in a future release + +Role Targets +^^^^^^^^^^^^ + +The lanuguage server is able to offer completions for the targets to a number of +different role types. + +.. figure:: ../../resources/images/complete-role-target-demo.gif + :align: center + + Completing role targets + +Currently supported roles include + +.. hlist:: + :columns: 3 + + * :rst:role:`sphinx:doc` + * :rst:role:`sphinx:envvar` + * :rst:role:`sphinx:ref` + * :rst:role:`sphinx:option` + * :rst:role:`sphinx:py:attr` + * :rst:role:`sphinx:py:class` + * :rst:role:`sphinx:py:data` + * :rst:role:`sphinx:py:exc` + * :rst:role:`sphinx:py:func` + * :rst:role:`sphinx:py:meth` + * :rst:role:`sphinx:py:mod` + * :rst:role:`sphinx:py:obj` + * :rst:role:`sphinx:term` + * :rst:role:`sphinx:token` + +Inter Sphinx +^^^^^^^^^^^^ + +The :doc:`intersphinx ` extension that +comes bundled with Sphinx makes it easy to link to other Sphinx projects. If +configured for your project, the language server will offer autocomplete +suggestions when appropriate. + +.. figure:: ../../resources/images/complete-intersphinx-demo.gif + :align: center + + Completing references to the Python documentation. + +Diagnostics +----------- + +The language server is able to catch some of the errors Sphinx outputs while +building and publish them as diagnostic messages + +.. figure:: ../../resources/images/diagnostic-sphinx-errors-demo.png + :align: center + + Example diagnostic messages from Sphinx \ No newline at end of file diff --git a/lib/esbonio/CHANGES.rst b/lib/esbonio/CHANGES.rst index cdae4bf8..2a88f153 100644 --- a/lib/esbonio/CHANGES.rst +++ b/lib/esbonio/CHANGES.rst @@ -15,7 +15,7 @@ Fixes - Errors encountered when initialising Sphinx are now caught and the language client is notified of an issue. (`#33 `_) -- **Language Server** Fix issue where some malformed ``CompletionItem``s were +- **Language Server** Fix issue where some malformed ``CompletionItem`` objects were preventing completion suggestions from being shown. (`#54 `_) - **Language Server** Windows paths are now handled correctly (`#60 `_) - **Language Server** Server no longer chooses ``conf.py`` files that diff --git a/lib/esbonio/changes/36.feature.rst b/lib/esbonio/changes/36.feature.rst new file mode 100644 index 00000000..c9643692 --- /dev/null +++ b/lib/esbonio/changes/36.feature.rst @@ -0,0 +1,2 @@ +**Language Server** Directive option completions are now provided + within a directive's options block diff --git a/lib/esbonio/changes/66.fix.rst b/lib/esbonio/changes/66.fix.rst new file mode 100644 index 00000000..6191add1 --- /dev/null +++ b/lib/esbonio/changes/66.fix.rst @@ -0,0 +1,3 @@ +**Language Server** Regex that catches diagnostics from Sphinx's + output can now handle windows paths. Diagnostic reporting now sends a + proper URI diff --git a/lib/esbonio/changes/68.fix.rst b/lib/esbonio/changes/68.fix.rst new file mode 100644 index 00000000..c606fe90 --- /dev/null +++ b/lib/esbonio/changes/68.fix.rst @@ -0,0 +1 @@ +**Language Server** Diagnostics are now reported on first startup diff --git a/lib/esbonio/changes/73.fix.rst b/lib/esbonio/changes/73.fix.rst new file mode 100644 index 00000000..0067f57d --- /dev/null +++ b/lib/esbonio/changes/73.fix.rst @@ -0,0 +1,2 @@ +**Language Server** Fix exception that was thrown when trying to find + completions for an unknown role type diff --git a/lib/esbonio/changes/74.feature.rst b/lib/esbonio/changes/74.feature.rst new file mode 100644 index 00000000..ff75ba9c --- /dev/null +++ b/lib/esbonio/changes/74.feature.rst @@ -0,0 +1,2 @@ +**Language Server** For projects that use ``interpshinx`` completions + for intersphinx targets are now suggested when available diff --git a/lib/esbonio/changes/77.fix.rst b/lib/esbonio/changes/77.fix.rst new file mode 100644 index 00000000..52935b01 --- /dev/null +++ b/lib/esbonio/changes/77.fix.rst @@ -0,0 +1,2 @@ +**Language Server** The server will not offer completion suggestions outside of +a role target \ No newline at end of file diff --git a/lib/esbonio/esbonio/lsp/__init__.py b/lib/esbonio/esbonio/lsp/__init__.py index d2fc9fc3..f42b092a 100644 --- a/lib/esbonio/esbonio/lsp/__init__.py +++ b/lib/esbonio/esbonio/lsp/__init__.py @@ -3,7 +3,7 @@ from typing import List -from pygls.features import COMPLETION, INITIALIZE, TEXT_DOCUMENT_DID_SAVE +from pygls.features import COMPLETION, INITIALIZE, INITIALIZED, TEXT_DOCUMENT_DID_SAVE from pygls.server import LanguageServer from pygls.types import ( CompletionList, @@ -35,6 +35,10 @@ def __init__(self, *args, **kwargs): self.on_init_hooks = [] """A list of functions to run on initialization""" + self.on_initialized_hooks = [] + """A list of functions to run after receiving the initialized notification from the + client""" + self.on_save_hooks = [] """A list of hooks to run on document save.""" @@ -47,15 +51,21 @@ def add_feature(self, feature): if hasattr(feature, "initialize"): self.on_init_hooks.append(feature.initialize) + if hasattr(feature, "initialized"): + self.on_initialized_hooks.append(feature.initialized) + if hasattr(feature, "save"): self.on_save_hooks.append(feature.save) - # TODO: Add support for mutltiple handlers using the same trigger. - if hasattr(feature, "suggest") and hasattr(feature, "suggest_trigger"): - trigger = feature.suggest_trigger - handler = feature.suggest + if hasattr(feature, "suggest") and hasattr(feature, "suggest_triggers"): + + for trigger in feature.suggest_triggers: + handler = feature.suggest - self.completion_handlers[trigger] = handler + if trigger in self.completion_handlers: + self.completion_handlers[trigger].append(handler) + else: + self.completion_handlers[trigger] = [handler] def load_module(self, mod: str): # TODO: Handle failures. @@ -101,6 +111,12 @@ def on_initialize(rst: RstLanguageServer, params: InitializeParams): rst.logger.info("LSP Server Initialized") + @server.feature(INITIALIZED) + def on_initialized(rst: RstLanguageServer, params): + + for initialized_hook in rst.on_initialized_hooks: + initialized_hook() + @server.feature(COMPLETION, trigger_characters=[".", ":", "`", "<"]) def on_completion(rst: RstLanguageServer, params: CompletionParams): """Suggest completions based on the current context.""" @@ -112,10 +128,11 @@ def on_completion(rst: RstLanguageServer, params: CompletionParams): items = [] - for pattern, handler in rst.completion_handlers.items(): + for pattern, handlers in rst.completion_handlers.items(): match = pattern.match(line) if match: - items += handler(match, line, doc) + for handler in handlers: + items += handler(match, doc, pos) return CompletionList(False, items) diff --git a/lib/esbonio/esbonio/lsp/completion/directives.py b/lib/esbonio/esbonio/lsp/completion/directives.py index 51e4110b..6fe069ce 100644 --- a/lib/esbonio/esbonio/lsp/completion/directives.py +++ b/lib/esbonio/esbonio/lsp/completion/directives.py @@ -11,8 +11,7 @@ from esbonio.lsp import RstLanguageServer -def to_completion_item(name: str, directive) -> CompletionItem: - """Convert an rst directive to its CompletionItem representation.""" +def resolve_directive(directive): # 'Core' docutils directives are returned as tuples (modulename, ClassName) # so its up to us to resolve the reference @@ -23,6 +22,13 @@ def to_completion_item(name: str, directive) -> CompletionItem: module = importlib.import_module(modulename) directive = getattr(module, cls) + return directive + + +def directive_to_completion_item(name: str, directive) -> CompletionItem: + """Convert an rst directive to its CompletionItem representation.""" + + directive = resolve_directive(directive) documentation = inspect.getdoc(directive) # TODO: Give better names to arguments based on what they represent. @@ -41,6 +47,23 @@ def to_completion_item(name: str, directive) -> CompletionItem: ) +def options_to_completion_items(directive) -> List[CompletionItem]: + """Convert a directive's options to a list of completion items.""" + + directive = resolve_directive(directive) + options = directive.option_spec + + if options is None: + return [] + + return [ + CompletionItem( + opt, detail="option", kind=CompletionItemKind.Field, insert_text=f"{opt}:" + ) + for opt in options + ] + + class DirectiveCompletion: """A completion handler for directives.""" @@ -68,24 +91,61 @@ def discover(self): dirs = {**dirs, **std_directives, **py_directives} self.directives = { - k: to_completion_item(k, v) + k: directive_to_completion_item(k, v) for k, v in dirs.items() if k != "restructuredtext-test-directive" } - self.rst.logger.debug("Discovered %s directives", len(self.directives)) - suggest_trigger = re.compile( - r""" - ^\s* # directives may be indented - \.\. # they start with an rst comment - [ ]* # followed by a space - ([\w-]+)?$ # with an optional name - """, - re.VERBOSE, - ) + self.options = { + k: options_to_completion_items(v) + for k, v in dirs.items() + if k in self.directives + } + + self.rst.logger.debug("Discovered %s directives", len(self.directives)) - def suggest(self, match, line, doc) -> List[CompletionItem]: - return list(self.directives.values()) + suggest_triggers = [ + re.compile( + r""" + ^\s* # directives may be indented + \.\. # they start with an rst comment + [ ]* # followed by a space + (?P[\w-]+)?$ # with an optional name + """, + re.VERBOSE, + ), + re.compile( + r""" + (?P\s+) # directive options must only be preceeded by whitespace + : # they start with a ':' + (?P[\w-]*) # they have a name + $ + """, + re.VERBOSE, + ), + ] + + def suggest(self, match, doc, position) -> List[CompletionItem]: + groups = match.groupdict() + + if "indent" not in groups: + return list(self.directives.values()) + + # Search backwards so that we can determine the context for our completion + indent = groups["indent"] + linum = position.line - 1 + line = doc.lines[linum] + + while line.startswith(indent): + linum -= 1 + line = doc.lines[linum] + + # Only offer completions if we're within a directive's option block + match = re.match(r"\s*\.\.[ ]*(?P[\w-]+)::", line) + if not match: + return [] + + return self.options.get(match.group("name"), []) def setup(rst: RstLanguageServer): diff --git a/lib/esbonio/esbonio/lsp/completion/roles.py b/lib/esbonio/esbonio/lsp/completion/roles.py index caae2ed5..cc730077 100644 --- a/lib/esbonio/esbonio/lsp/completion/roles.py +++ b/lib/esbonio/esbonio/lsp/completion/roles.py @@ -10,6 +10,14 @@ from esbonio.lsp import RstLanguageServer +def namespace_to_completion_item(namespace: str) -> CompletionItem: + return CompletionItem( + namespace, + detail="intersphinx namespace", + kind=CompletionItemKind.Module, + ) + + def role_to_completion_item(name, role) -> CompletionItem: return CompletionItem( name, @@ -27,6 +35,14 @@ def role_to_completion_item(name, role) -> CompletionItem: "function": CompletionItemKind.Function, "method": CompletionItemKind.Method, "module": CompletionItemKind.Module, + "py:attribute": CompletionItemKind.Field, + "py:class": CompletionItemKind.Class, + "py:function": CompletionItemKind.Function, + "py:method": CompletionItemKind.Method, + "py:module": CompletionItemKind.Module, + "std:doc": CompletionItemKind.File, + "std:envvar": CompletionItemKind.Variable, + "std:term": CompletionItemKind.Text, "term": CompletionItemKind.Text, } @@ -36,6 +52,21 @@ def target_to_completion_item(name, display, type_) -> CompletionItem: return CompletionItem(name, kind=kind, detail=str(display), insert_text=name) +def intersphinx_target_to_completion_item(label, item, type_) -> CompletionItem: + kind = TARGET_KINDS.get(type_, CompletionItemKind.Reference) + source, version, _, display = item + + if display == "-": + display = label + + if version: + version = f" v{version}" + + detail = f"{display} - {source}{version}" + + return CompletionItem(label, kind=kind, detail=detail, insert_text=label) + + class RoleCompletion: """Completion handler for roles.""" @@ -72,18 +103,40 @@ def discover(self): self.roles = {k: role_to_completion_item(k, v) for k, v in rs.items()} self.rst.logger.debug("Discovered %s roles", len(self.roles)) - suggest_trigger = re.compile( - r""" - (^|.*[ ]) # roles must be preceeded by a space, or start the line - : # roles start with the ':' character - (?!:) # make sure the next character is not ':' - [\w-]* # match the role name - $ # ensure pattern only matches incomplete roles - """, - re.MULTILINE | re.VERBOSE, - ) + suggest_triggers = [ + re.compile( + r""" + (^|.*[ ]) # roles must be preceeded by a space, or start the line + : # roles start with the ':' character + (?!:) # make sure the next character is not ':' + [\w-]* # match the role name + $ # ensure pattern only matches incomplete roles + """, + re.MULTILINE | re.VERBOSE, + ) + ] + + def suggest(self, match, doc, position) -> List[CompletionItem]: + indent = match.group(1) + + # If there's no indent, then this can only be a role defn + if indent == "": + return list(self.roles.values()) + + # Otherwise, search backwards until we find a blank line or an unindent + # so that we can determine the appropriate context. + linum = position.line - 1 + line = doc.lines[linum] + + while line.startswith(indent): + linum -= 1 + line = doc.lines[linum] + + # Unless we are within a directive's options block, we should offer role + # suggestions + if re.match(r"\s*\.\.[ ]*([\w-]+)::", line): + return [] - def suggest(self, match, line, doc) -> List[CompletionItem]: return list(self.roles.values()) @@ -139,18 +192,35 @@ def initialize(self): def save(self, params: DidSaveTextDocumentParams): self.discover_targets() - suggest_trigger = re.compile( - r""" - (^|.*[ ]) # roles must be preveeded by a space, or start the line - : # roles start with the ':' character - (?P[\w-]+) # capture the role name, suggestions will change based on it - : # the role name ends with a ':' - ` # the target begins with a '`'` - """, - re.MULTILINE | re.VERBOSE, - ) - - def suggest(self, match, line, doc) -> List[CompletionItem]: + suggest_triggers = [ + re.compile( + r""" + (^|.*[ ]) # roles must be preceeded by a space, or start the line + : # roles start with the ':' character + (?P[\w-]+) # capture the role name, suggestions will change based on it + : # the role name ends with a ':' + ` # the target begins with a '`' + (?P[^<:`]*) # match "plain link" targets + $ + """, + re.MULTILINE | re.VERBOSE, + ), + re.compile( + r""" + (^|.*[ ]) # roles must be preceeded by a space, or start the line + : # roles start with the ':' character + (?P[\w-]+) # capture the role name, suggestions will change based on it + : # the role name ends with a ':' + ` # the target begins with a '`'` + .*< # the actual target name starts after a '<' + (?P[^`:]*) # match "aliased" targets + $ + """, + re.MULTILINE | re.VERBOSE, + ), + ] + + def suggest(self, match, doc, position) -> List[CompletionItem]: # TODO: Detect if we're in an angle bracket e.g. :ref:`More Info <|` in that # situation, add the closing '>' to the completion item insert text. @@ -161,7 +231,7 @@ def suggest(self, match, line, doc) -> List[CompletionItem]: types = self.target_types.get(rolename, None) if types is None: - return None + return [] targets = [] for type_ in types: @@ -195,9 +265,132 @@ def discover_targets(self): self.targets = {**build_target_map(py), **build_target_map(std)} +class InterSphinxNamespaceCompletion: + """Completion handler for intersphinx namespaces.""" + + def __init__(self, rst: RstLanguageServer): + self.rst = rst + self.namespaces = {} + + def initialize(self): + + if self.rst.app and hasattr(self.rst.app.env, "intersphinx_named_inventory"): + inv = self.rst.app.env.intersphinx_named_inventory + self.namespaces = {v: namespace_to_completion_item(v) for v in inv.keys()} + + self.rst.logger.debug( + "Discovered %s intersphinx namespaces", len(self.namespaces) + ) + + suggest_triggers = RoleTargetCompletion.suggest_triggers + + def suggest(self, match, doc, position) -> List[CompletionItem]: + return list(self.namespaces.values()) + + +def build_target_type_map(domain: Domain) -> Dict[str, List[str]]: + + types = {} + + for name, obj in domain.object_types.items(): + for role in obj.roles: + objs = types.get(role, None) + + if objs is None: + objs = [] + + objs.append(f"{domain.name}:{name}") + types[role] = objs + + return types + + +class InterSphinxTargetCompletion: + """Completion handler for intersphinx targets""" + + def __init__(self, rst: RstLanguageServer): + self.rst = rst + self.targets = {} + self.target_types = {} + + def initialize(self): + + if self.rst.app and hasattr(self.rst.app.env, "intersphinx_named_inventory"): + inv = self.rst.app.env.intersphinx_named_inventory + domains = self.rst.app.env.domains + + for domain in domains.values(): + self.target_types.update(build_target_type_map(domain)) + + for namespace, types in inv.items(): + self.targets[namespace] = { + type_: { + label: intersphinx_target_to_completion_item(label, item, type_) + for label, item in items.items() + } + for type_, items in types.items() + } + + suggest_triggers = [ + re.compile( + r""" + (^|.*[ ]) # roles must be preceeded by a space, or start the line + : # roles start with the ':' character + (?P[\w-]+) # capture the role name, suggestions will change based on it + : # the role name ends with a ':' + ` # the target begins with a '`' + (?P[^<:]*) # match "plain link" targets + : # namespaces end with a ':' + $ + """, + re.MULTILINE | re.VERBOSE, + ), + re.compile( + r""" + (^|.*[ ]) # roles must be preceeded by a space, or start the line + : # roles start with the ':' character + (?P[\w-]+) # capture the role name, suggestions will change based on it + : # the role name ends with a ':' + ` # the target begins with a '`'` + .*< # the actual target name starts after a '<' + (?P[^:]*) # match "aliased" targets + : # namespaces end with a ':' + $ + """, + re.MULTILINE | re.VERBOSE, + ), + ] + + def suggest(self, match, doc, position) -> List[CompletionItem]: + # TODO: Detect if we're in an angle bracket e.g. :ref:`More Info <|` in that + # situation, add the closing '>' to the completion item insert text. + + namespace = match.group("namespace") + rolename = match.group("name") + + types = self.target_types.get(rolename, None) + if types is None: + return [] + + namespace = self.targets.get(namespace, None) + if namespace is None: + return [] + + targets = [] + for type_ in types: + items = namespace.get(type_, {}) + targets += items.values() + + return targets + + def setup(rst: RstLanguageServer): role_completion = RoleCompletion(rst) role_target_completion = RoleTargetCompletion(rst) + intersphinx_namespaces = InterSphinxNamespaceCompletion(rst) + intersphinx_targets = InterSphinxTargetCompletion(rst) rst.add_feature(role_completion) rst.add_feature(role_target_completion) + rst.add_feature(intersphinx_namespaces) + rst.add_feature(intersphinx_targets) diff --git a/lib/esbonio/esbonio/lsp/sphinx.py b/lib/esbonio/esbonio/lsp/sphinx.py index 85d37700..4e7bab69 100644 --- a/lib/esbonio/esbonio/lsp/sphinx.py +++ b/lib/esbonio/esbonio/lsp/sphinx.py @@ -24,7 +24,7 @@ PROBLEM_PATTERN = re.compile( r""" - (?P[^:]*):(?P\d+):\s(?P[^:]*):(\s+)?(?P.*) + (?P(.*:\\)?[^:]*):(?P\d+):\s(?P[^:]*):(\s+)?(?P.*) """, re.VERBOSE, ) @@ -74,10 +74,21 @@ def __init__(self, rst: RstLanguageServer): def initialize(self): self.create_app() - self.refresh_app() + + if self.rst.app is not None: + self.rst.app.builder.read() + + def initialized(self): + self.report_diagnostics() def save(self, params: DidSaveTextDocumentParams): - self.refresh_app() + + if self.rst.app is None: + return + + self.reset_diagnostics() + self.rst.app.builder.read() + self.report_diagnostics() def create_app(self): """Initialize a Sphinx application instance for the current workspace.""" @@ -122,15 +133,16 @@ def create_app(self): msg_type=MessageType.Error, ) - def refresh_app(self): - if self.rst.app is None: - return - - self.reset_diagnostics() - self.rst.app.builder.read() + def report_diagnostics(self): + """Publish the current set of diagnostics to the client.""" for doc, diagnostics in self.diagnostics.items(): - self.rst.publish_diagnostics(doc, diagnostics) + + if not doc.startswith("/"): + doc = "/" + doc + + uri = f"file://{doc}" + self.rst.publish_diagnostics(uri, diagnostics) def reset_diagnostics(self): """Reset the list of diagnostics.""" diff --git a/lib/esbonio/setup.cfg b/lib/esbonio/setup.cfg index 98537b4a..efd641d5 100644 --- a/lib/esbonio/setup.cfg +++ b/lib/esbonio/setup.cfg @@ -34,4 +34,5 @@ dev = black ; flake8 ; pytest ; pytest-cov ; mock lsp = appdirs ; pygls [flake8] -max-line-length = 88 \ No newline at end of file +max-line-length = 88 +ignore = E501 \ No newline at end of file diff --git a/lib/esbonio/tests/conftest.py b/lib/esbonio/tests/conftest.py index 1ea8aa2d..f28c9a85 100644 --- a/lib/esbonio/tests/conftest.py +++ b/lib/esbonio/tests/conftest.py @@ -1,9 +1,7 @@ import asyncio -import logging import os import pathlib import threading -import time import unittest.mock as mock import py.test @@ -26,7 +24,7 @@ def client_server(): """A fixture that sets up an LSP server + client. - Originally based on https://github.com/openlawlibrary/pygls/blob/59f3056baa4de4c4fb374d3657194f2669c174bc/tests/conftest.py + Originally based on https://github.com/openlawlibrary/pygls/blob/59f3056baa4de4c4fb374d3657194f2669c174bc/tests/conftest.py # noqa: E501 """ # Pipes so that client + server can communicate diff --git a/lib/esbonio/tests/data/sphinx-default/directive_options.rst b/lib/esbonio/tests/data/sphinx-default/directive_options.rst new file mode 100644 index 00000000..71f4118e --- /dev/null +++ b/lib/esbonio/tests/data/sphinx-default/directive_options.rst @@ -0,0 +1,2 @@ +.. image:: filename.png + :alt: test diff --git a/lib/esbonio/tests/data/sphinx-extensions/.vscode/settings.json b/lib/esbonio/tests/data/sphinx-extensions/.vscode/settings.json new file mode 100644 index 00000000..18bac824 --- /dev/null +++ b/lib/esbonio/tests/data/sphinx-extensions/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.pythonPath": "${workspaceRoot}/../../../../../.env/bin/python", + "python.formatting.provider": "black" +} \ No newline at end of file diff --git a/lib/esbonio/tests/data/sphinx-extensions/Makefile b/lib/esbonio/tests/data/sphinx-extensions/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/lib/esbonio/tests/data/sphinx-extensions/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/lib/esbonio/tests/data/sphinx-extensions/conf.py b/lib/esbonio/tests/data/sphinx-extensions/conf.py new file mode 100644 index 00000000..716017aa --- /dev/null +++ b/lib/esbonio/tests/data/sphinx-extensions/conf.py @@ -0,0 +1,54 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +# -- Project information ----------------------------------------------------- + +project = "Defaults" +copyright = "2020, Sphinx" +author = "Sphinx" + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["sphinx.ext.doctest", "sphinx.ext.intersphinx"] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master", None), +} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] diff --git a/lib/esbonio/tests/data/sphinx-extensions/glossary.rst b/lib/esbonio/tests/data/sphinx-extensions/glossary.rst new file mode 100644 index 00000000..796f635b --- /dev/null +++ b/lib/esbonio/tests/data/sphinx-extensions/glossary.rst @@ -0,0 +1,10 @@ +Glossary +======== + +.. glossary:: + + hypotenuse + The longest side of a triangle + + right angle + An angle of 90 degrees \ No newline at end of file diff --git a/lib/esbonio/tests/data/sphinx-extensions/index.rst b/lib/esbonio/tests/data/sphinx-extensions/index.rst new file mode 100644 index 00000000..fd10b88b --- /dev/null +++ b/lib/esbonio/tests/data/sphinx-extensions/index.rst @@ -0,0 +1,32 @@ +.. Defaults documentation master file, created by + sphinx-quickstart on Wed Dec 2 22:54:25 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +.. _welcome: + +Welcome to Defaults's documentation! +==================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + theorems/index + glossary + +Setup +===== + +In order to run the program you need a few environment variables set. + +.. envvar:: ANGLE_UNIT + + Use this environment variable to set the unit used when describing angles. Valid + values are ``degress``, ``radians`` or ``gradians``. + +.. envvar:: PRECISION + + Use this to set the level of precision used when manipulating floating point numbers. + Its value is an integer which represents the number of decimal places to use, default + value is ``2`` diff --git a/lib/esbonio/tests/data/sphinx-extensions/make.bat b/lib/esbonio/tests/data/sphinx-extensions/make.bat new file mode 100644 index 00000000..2119f510 --- /dev/null +++ b/lib/esbonio/tests/data/sphinx-extensions/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/lib/esbonio/tests/data/sphinx-extensions/theorems/index.rst b/lib/esbonio/tests/data/sphinx-extensions/theorems/index.rst new file mode 100644 index 00000000..14c0817b --- /dev/null +++ b/lib/esbonio/tests/data/sphinx-extensions/theorems/index.rst @@ -0,0 +1,8 @@ +Theorems +======== + +There are many useful theorems, you will find some of them here. + +.. toctree:: + + pythagoras \ No newline at end of file diff --git a/lib/esbonio/tests/data/sphinx-extensions/theorems/pythagoras.rst b/lib/esbonio/tests/data/sphinx-extensions/theorems/pythagoras.rst new file mode 100644 index 00000000..e6177ec1 --- /dev/null +++ b/lib/esbonio/tests/data/sphinx-extensions/theorems/pythagoras.rst @@ -0,0 +1,64 @@ +.. _pythagoras_theorem: + +Pythagoras' Theorem +=================== + +Pythagoras' Theorem describes the relationship between the length of the +sides of a right angled triangle. + +Implementation +-------------- + +This project provides some functions which use Pythagoras' Theorem to calculate the +length of a missing side of a right angled triangle when the other two are known. + +.. module:: pythagoras + +.. currentmodule:: pythagoras + +.. data:: PI + + The value of the constant pi. + +.. data:: UNKNOWN + + Used to represent an unknown value. + +.. class:: Triangle(a: float, b: float, c: float) + + Represents a triangle + + .. attribute:: a + + The length of the side labelled ``a`` + + .. attribute:: b + + The length of the side labelled ``b``` + + .. attribute:: c + + The length of the side labelled ``c`` + + .. method:: is_right_angled() -> bool + + :return: :code:`True` if the triangle is right angled. + :rtype: bool + +.. function:: calc_hypotenuse(a: float, b: float) -> float + + Calculates the length of the hypotenuse of a right angled triangle. + + :param float a: The length of the side labelled ``a`` + :param float b: The length of the side labelled ``b`` + :return: Then length of the side ``c`` (the triangle's hypotenuse) + :rtype: float + +.. function:: calc_side(c: float, b: float) -> float + + Calculates the length of a side of a right angled triangle. + + :param float c: The length of the side labelled ``c`` (the triangle's hypotenuse) + :param float b: The length of the side labelled ``b`` + :return: Then length of the side ``a`` + :rtype: float diff --git a/lib/esbonio/tests/lsp/completion/test_integration.py b/lib/esbonio/tests/lsp/completion/test_integration.py index e6fe34c0..d1169a31 100644 --- a/lib/esbonio/tests/lsp/completion/test_integration.py +++ b/lib/esbonio/tests/lsp/completion/test_integration.py @@ -1,6 +1,10 @@ +import logging import itertools +import pathlib import time +from typing import Set + import py.test from pygls.features import ( @@ -9,6 +13,7 @@ TEXT_DOCUMENT_DID_CHANGE, TEXT_DOCUMENT_DID_OPEN, ) +from pygls.server import LanguageServer from pygls.types import ( CompletionContext, CompletionParams, @@ -23,56 +28,121 @@ VersionedTextDocumentIdentifier, ) -WAIT = 0.1 - -# Expected directive suggestions (not exhaustive) -DIRECTIVES = { - "admonition", - "attention", - "attribute", - "classmethod", - "code-block", - "envvar", - "figure", - "glossary", - "hlist", - "image", - "include", - "index", - "line-block", - "list-table", - "literalinclude", - "toctree", -} - -# Expected role suggestions (not exhaustive) -ROLES = {"class", "doc", "func", "ref", "term"} - - -# Expected (:py):class: target suggestions -CLASS_TARGETS = {"pythagoras.Triangle"} - -# Expected :doc: target suggestions -DOC_TARGETS = {"index", "glossary", "theorems/index", "theorems/pythagoras"} - -# Expected (:py):func: target suggestions -FUNC_TARGETS = {"pythagoras.calc_hypotenuse", "pythagoras.calc_side"} - -# Expected (:py):meth: target suggestions -METH_TARGETS = {"pythagoras.Triangle.is_right_angled"} - -# Expected :ref: target suggestions -REF_TARGETS = { - "genindex", - "modindex", - "py-modindex", - "pythagoras_theorem", - "search", - "welcome", -} - - -def role_target_examples(rolename): +WAIT = 0.1 # How long should we sleep after an lsp.notify(...) + + +def do_completion_test( + client: LanguageServer, + server: LanguageServer, + root: pathlib.Path, + filename: str, + text: str, + expected: Set[str], + insert_newline: bool = True, +): + """A generic helper for performing completion tests. + + Being an integration test, it is quite involved as it has to use the protocol to + take the server to a point where it can provide completion suggestions. As part of + the setup, this helper + + - Sends an 'initialize' request to the server, setting the workspace root. + - Sends a 'textDocument/didOpen' notification, loading the specified document in the + server's workspace + - Sends a 'textDocument/didChange' notification, inserting the text we want + suggestions for + - Sends a 'completion' request, and ensures that the expected completed items are in + the response + + Currently this method is only capable of ensuring that item labels are as expected, + none of the other CompletionItem fields are inspected. This method is also not capable + of ensuring particular items are NOT suggested. + + Parameters + ---------- + client: + The client LanguageServer instance + server: + The server LanguageServer instance + root: + The directory to use as the workspace root + filename: + The file to open for the test, relative to the workspace root + text: + The text to insert, this is the text this method requests completions for. + Note this CANNOT contain any newlines. + expected: + The CompletionItem labels that should be returned. This does not have to be + exhaustive. + insert_newline: + Flag to indicate if a newline should be inserted before the given ``text`` + """ + + # Initialize the language server. + response = client.lsp.send_request( + INITIALIZE, {"processId": 1234, "rootUri": root.as_uri(), "capabilities": None} + ).result(timeout=2) + + # Ensure that the server has configured itself correctly. + assert server.workspace.root_uri == root.as_uri() + + # Ensure that server broadcasted the fact that it supports completions. + provider = response.capabilities.completionProvider + assert set(provider.triggerCharacters) == {".", ":", "`", "<"} + + # Let the server know that we have recevied the response. + # client.lsp.notify(INITIALIZED) + # time.sleep(WAIT) + + # Let's open a file to edit. + testfile = root / filename + testuri = testfile.as_uri() + content = testfile.read_text() + + client.lsp.notify( + TEXT_DOCUMENT_DID_OPEN, + DidOpenTextDocumentParams(TextDocumentItem(testuri, "rst", 1, content)), + ) + + time.sleep(WAIT) + assert len(server.lsp.workspace.documents) == 1 + + # With the setup out of the way, let's type the text we want completion suggestions + # for + start = len(content.splitlines()) + insert_newline + text = "\n" + text if insert_newline else text + + client.lsp.notify( + TEXT_DOCUMENT_DID_CHANGE, + DidChangeTextDocumentParams( + VersionedTextDocumentIdentifier(testuri, 2), + [ + TextDocumentContentChangeEvent( + Range(Position(start, 0), Position(start, 0)), text=text + ) + ], + ), + ) + + time.sleep(WAIT) + # Now make the completion request and check to make sure we get the appropriate + # response + response = client.lsp.send_request( + COMPLETION, + CompletionParams( + TextDocumentIdentifier(testuri), + Position(start, len(text) + 1), + CompletionContext(trigger_kind=CompletionTriggerKind.Invoked), + ), + ).result(timeout=2) + + actual = {item.label for item in response.items} + missing = expected - actual + + assert len(missing) == 0, "Missing expected items, {}".format(missing) + + +def role_target_patterns(rolename): return [ s.format(rolename) for s in [ @@ -84,9 +154,21 @@ def role_target_examples(rolename): ] +def intersphinx_patterns(rolename, namespace): + return [ + s.format(rolename, namespace) + for s in [ + ":{}:`{}:", + ":{}:`More Info <{}:", + " :{}:`{}:", + " :{}:`Some Label <{}:", + ] + ] + + @py.test.mark.integration @py.test.mark.parametrize( - "text,expected", + "text,setup", [ *itertools.product( [ @@ -99,7 +181,7 @@ def role_target_examples(rolename): ".. _some_label:", " .. _some_label:", ], - [set()], + [("sphinx-default", set())], ), *itertools.product( [ @@ -112,109 +194,215 @@ def role_target_examples(rolename): " .. d", " .. code-b", ], - [DIRECTIVES], + [ + ( + "sphinx-default", + {"admonition", "classmethod", "code-block", "image", "toctree"}, + ), + ( + "sphinx-extensions", + { + "admonition", + "classmethod", + "code-block", + "doctest", + "image", + "testsetup", + "toctree", + }, + ), + ], ), *itertools.product( + [":", ":r", "some text :", " :", " :r", " some text :"], [ - ":", - ":r", - "some text :", - " :", - " :r", - " some text :", + ("sphinx-default", {"class", "doc", "func", "ref", "term"}), ], - [ROLES], ), *itertools.product( - role_target_examples("class"), - [CLASS_TARGETS], + role_target_patterns("class"), + [ + ("sphinx-default", {"pythagoras.Triangle"}), + ("sphinx-extensions", {"pythagoras.Triangle", "python", "sphinx"}), + ], ), *itertools.product( - role_target_examples("doc"), - [DOC_TARGETS], + role_target_patterns("doc"), + [ + ( + "sphinx-default", + {"index", "glossary", "theorems/index", "theorems/pythagoras"}, + ), + ( + "sphinx-extensions", + { + "index", + "glossary", + "python", + "sphinx", + "theorems/index", + "theorems/pythagoras", + }, + ), + ], ), *itertools.product( - role_target_examples("func"), - [FUNC_TARGETS], + role_target_patterns("func"), + [ + ( + "sphinx-default", + {"pythagoras.calc_hypotenuse", "pythagoras.calc_side"}, + ), + ( + "sphinx-extensions", + { + "pythagoras.calc_hypotenuse", + "pythagoras.calc_side", + "python", + "sphinx", + }, + ), + ], + ), + *itertools.product( + role_target_patterns("meth"), + [ + ("sphinx-default", {"pythagoras.Triangle.is_right_angled"}), + ( + "sphinx-extensions", + {"pythagoras.Triangle.is_right_angled", "python", "sphinx"}, + ), + ], + ), + *itertools.product( + role_target_patterns("obj"), + [ + ( + "sphinx-default", + { + "pythagoras.Triangle", + "pythagoras.Triangle.is_right_angled", + "pythagoras.calc_hypotenuse", + "pythagoras.calc_side", + }, + ), + ( + "sphinx-extensions", + { + "pythagoras.Triangle", + "pythagoras.Triangle.is_right_angled", + "pythagoras.calc_hypotenuse", + "pythagoras.calc_side", + "python", + "sphinx", + }, + ), + ], ), *itertools.product( - role_target_examples("meth"), - [METH_TARGETS], + role_target_patterns("ref"), + [ + ( + "sphinx-default", + { + "genindex", + "modindex", + "py-modindex", + "pythagoras_theorem", + "search", + "welcome", + }, + ), + ( + "sphinx-extensions", + { + "genindex", + "modindex", + "py-modindex", + "pythagoras_theorem", + "python", + "sphinx", + "search", + "welcome", + }, + ), + ], + ), + *itertools.product( + intersphinx_patterns("ref", "python"), + [ + ("sphinx-default", set()), + ( + "sphinx-extensions", + {"configparser-objects", "types", "whatsnew-index"}, + ), + ], + ), + *itertools.product( + intersphinx_patterns("class", "python"), + [ + ("sphinx-default", set()), + ( + "sphinx-extensions", + {"abc.ABCMeta", "logging.StreamHandler", "zipfile.ZipInfo"}, + ), + ], ), *itertools.product( - role_target_examples("obj"), - [CLASS_TARGETS, FUNC_TARGETS, METH_TARGETS], + intersphinx_patterns("ref", "sphinx"), + [ + ("sphinx-default", set()), + ( + "sphinx-extensions", + { + "basic-domain-markup", + "extension-tutorials-index", + "writing-builders", + }, + ), + ], ), *itertools.product( - role_target_examples("ref"), - [REF_TARGETS], + intersphinx_patterns("class", "sphinx"), + [ + ("sphinx-default", set()), + ( + "sphinx-extensions", + { + "sphinx.addnodes.desc", + "sphinx.builders.Builder", + "sphinxcontrib.websupport.WebSupport", + }, + ), + ], ), ], ) -def test_completion(client_server, testdata, text, expected): +def test_expected_completions(client_server, testdata, text, setup): """Ensure that we can offer the correct completion suggestions.""" client, server = client_server - root = testdata("sphinx-default", path_only=True) + project, expected = setup + root = testdata(project, path_only=True) - # Initialize the language server. - response = client.lsp.send_request( - INITIALIZE, {"processId": 1234, "rootUri": root.as_uri(), "capabilities": None} - ).result(timeout=2) + do_completion_test(client, server, root, "index.rst", text, expected) - # Ensure that the server has configured itself correctly. - assert server.workspace.root_uri == root.as_uri() - # Ensure that server broadcasted the fact that it supports completions. - provider = response.capabilities.completionProvider - assert set(provider.triggerCharacters) == {".", ":", "`", "<"} +def test_expected_directive_option_completions(client_server, testdata, caplog): + """Ensure that we can handle directive option completions.""" - # Let the server know that we have recevied the response. - # client.lsp.notify(INITIALIZED) - # time.sleep(WAIT) + caplog.set_level(logging.INFO) - # Let's open a file to edit. - testfile = root / "index.rst" - testuri = testfile.as_uri() - content = testfile.read_text() + client, server = client_server + root = testdata("sphinx-default", path_only=True) + expected = {"align", "alt", "class", "height", "name", "scale", "target", "width"} - client.lsp.notify( - TEXT_DOCUMENT_DID_OPEN, - DidOpenTextDocumentParams(TextDocumentItem(testuri, "rst", 1, content)), + do_completion_test( + client, + server, + root, + "directive_options.rst", + " :a", + expected, + insert_newline=False, ) - - time.sleep(WAIT) - assert len(server.lsp.workspace.documents) == 1 - - # With the setup out of the way, let's type the text we want completion suggestions - # for - start = len(content.splitlines()) + 1 - - client.lsp.notify( - TEXT_DOCUMENT_DID_CHANGE, - DidChangeTextDocumentParams( - VersionedTextDocumentIdentifier(testuri, 2), - [ - TextDocumentContentChangeEvent( - Range(Position(start, 0), Position(start, 0)), text="\n" + text - ) - ], - ), - ) - - time.sleep(WAIT) - # Now make the completion request and check to make sure we get the appropriate - # response - response = client.lsp.send_request( - COMPLETION, - CompletionParams( - TextDocumentIdentifier(testuri), - Position(start, len(text) + 1), - CompletionContext(trigger_kind=CompletionTriggerKind.Invoked), - ), - ).result(timeout=2) - - actual = {item.label for item in response.items} - missing = expected - actual - - assert len(missing) == 0, "Missing expected items, {}".format(missing) diff --git a/lib/esbonio/tests/lsp/completion/test_roles.py b/lib/esbonio/tests/lsp/completion/test_roles.py index 4f218e8a..25412a7b 100644 --- a/lib/esbonio/tests/lsp/completion/test_roles.py +++ b/lib/esbonio/tests/lsp/completion/test_roles.py @@ -101,7 +101,13 @@ def test_target_type_discovery(sphinx, role, objects): "sphinx-default", "doc", CompletionItemKind.File, - {"glossary", "index", "theorems/index", "theorems/pythagoras"}, + { + "glossary", + "index", + "theorems/index", + "theorems/pythagoras", + "directive_options", + }, ), ( "sphinx-default", diff --git a/lib/esbonio/tests/lsp/test_sphinx.py b/lib/esbonio/tests/lsp/test_sphinx.py index 4406c727..7e5d1488 100644 --- a/lib/esbonio/tests/lsp/test_sphinx.py +++ b/lib/esbonio/tests/lsp/test_sphinx.py @@ -63,6 +63,19 @@ def test_find_conf_py(root, candidates, expected): ] }, ), + ( + "c:\\path\\to\\file.rst:4: WARNING: toctree contains reference to nonexisting document 'changelog'", + { + "c:\\path\\to\\file.rst": [ + Diagnostic( + line(4), + "toctree contains reference to nonexisting document 'changelog'", + severity=DiagnosticSeverity.Warning, + source="sphinx", + ) + ] + }, + ), ( "/path/to/file.rst:120: ERROR: unable to build docs", { @@ -76,6 +89,19 @@ def test_find_conf_py(root, candidates, expected): ] }, ), + ( + "c:\\path\\to\\file.rst:120: ERROR: unable to build docs", + { + "c:\\path\\to\\file.rst": [ + Diagnostic( + line(120), + "unable to build docs", + severity=DiagnosticSeverity.Error, + source="sphinx", + ) + ] + }, + ), ( "/path/to/file.rst:71: WARNING: duplicate label: _setup", { @@ -89,6 +115,19 @@ def test_find_conf_py(root, candidates, expected): ] }, ), + ( + "c:\\path\\to\\file.rst:71: WARNING: duplicate label: _setup", + { + "c:\\path\\to\\file.rst": [ + Diagnostic( + line(71), + "duplicate label: _setup", + severity=DiagnosticSeverity.Warning, + source="sphinx", + ) + ] + }, + ), ], ) def test_parse_diagnostics(text, expected): @@ -116,3 +155,27 @@ def test_parse_diagnostics(text, expected): # Ensure that can correctly reset diagnostics management.reset_diagnostics() assert management.diagnostics == {file: [] for file in expected.keys()} + + +def test_report_diagnostics(): + """Ensure that diagnostic, filepaths are correctly transformed into uris.""" + + publish_diagnostics = mock.Mock() + + rst = mock.Mock() + rst.publish_diagnostics = publish_diagnostics + + manager = SphinxManagement(rst) + manager.diagnostics = { + "c:\\Users\\username\\Project\\file.rst": (1, 2, 3), + "/home/username/Project/file.rst": (4, 5, 6), + } + + manager.reset_diagnostics = mock.Mock() + manager.save(None) + + expected = [ + mock.call("file:///c:\\Users\\username\\Project\\file.rst", (1, 2, 3)), + mock.call("file:///home/username/Project/file.rst", (4, 5, 6)), + ] + assert publish_diagnostics.call_args_list == expected diff --git a/resources/images/complete-directive-demo.gif b/resources/images/complete-directive-demo.gif new file mode 100644 index 00000000..3b5be29d Binary files /dev/null and b/resources/images/complete-directive-demo.gif differ diff --git a/resources/images/complete-directive-options-demo.gif b/resources/images/complete-directive-options-demo.gif new file mode 100644 index 00000000..8b2f1595 Binary files /dev/null and b/resources/images/complete-directive-options-demo.gif differ diff --git a/resources/images/complete-intersphinx-demo.gif b/resources/images/complete-intersphinx-demo.gif new file mode 100644 index 00000000..9698cbbf Binary files /dev/null and b/resources/images/complete-intersphinx-demo.gif differ diff --git a/resources/images/complete-role-demo.gif b/resources/images/complete-role-demo.gif new file mode 100644 index 00000000..a57cdeee Binary files /dev/null and b/resources/images/complete-role-demo.gif differ diff --git a/resources/images/complete-role-target-demo.gif b/resources/images/complete-role-target-demo.gif new file mode 100644 index 00000000..cd8f6762 Binary files /dev/null and b/resources/images/complete-role-target-demo.gif differ diff --git a/resources/images/diagnostic-sphinx-errors-demo.png b/resources/images/diagnostic-sphinx-errors-demo.png new file mode 100644 index 00000000..4f9e4530 Binary files /dev/null and b/resources/images/diagnostic-sphinx-errors-demo.png differ