diff --git a/docs/lsp/_architecture.rst b/docs/lsp/_architecture.rst deleted file mode 100644 index cb957f26e..000000000 --- a/docs/lsp/_architecture.rst +++ /dev/null @@ -1,230 +0,0 @@ -.. raw:: html - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - Language Feature #1 - Language Feature #2 - Language Server - Language Client - LSP Protocol - - - Engine - - ..... - Language Feature #N - - - - - -

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

-
diff --git a/docs/lsp/extending.rst b/docs/lsp/extending.rst index 7ddf6a7a1..362a8d438 100644 --- a/docs/lsp/extending.rst +++ b/docs/lsp/extending.rst @@ -3,68 +3,4 @@ Extending ========= -In order to support the extensible nature of reStructuredText and Sphinx, Esbonio itself is structured so that it can be easily extended. -This section of the documentation outlines the server's architecture and how you can write your own extensions. - -.. toctree:: - :maxdepth: 1 - - extending/directives - extending/roles - extending/api-reference - -.. _lsp_architecture: - -Architecture ------------- - -.. include:: ./_architecture.rst - -.. glossary:: - - Language Server - A language server is a subclass of the ``LanguageServer`` class provided by the `pygls`_ library. - - In Esbonio, all the features you would typically associate with a language server, e.g. completions are not actually implemented by the language server. - These features are provided through a number of "language features" (see below). - Instead a language server acts a container for all the active language features and provides an API they can use to query aspects of the environment. - - Esbonio currently provides two language servers - - - :class:`~esbonio.lsp.rst.RstLanguageServer`: Base language server, meant for "vanilla" docutils projects. - - :class:`~esbonio.lsp.sphinx.SphinxLanguageServer` Language server, specialising in Sphinx projects. - - Language Feature - Language features are subclasses of :class:`~esbonio.lsp.rst.LanguageFeature`. - They are typically based on a single aspect of reStructuredText (e.g. :class:`~esbonio.lsp.roles.Roles`). - - Language Features (where it makes sense) should be server agnostic, that way the same features can be reused across different envrionments. - - Engine - For lack of a better name... an "engine" is responsible for mapping messages from the LSP Protocol into function calls within the language server. - Unlike the other components of the architecture, an "engine" isn't formally defined and there is no API to implement. - Instead it's just the term used to refer to all the ``@server.feature()`` handlers that define how LSP messages should be handled. - - Currently we provide just a single "engine" :func:`~esbonio.lsp.create_language_server`. - As an example, here is how it handles ``textDocument/completion`` requests. - - .. literalinclude:: ../../lib/esbonio/esbonio/lsp/__init__.py - :language: python - :dedent: - :start-after: # - :end-before: # - - There is nothing in Esbonio that would prevent you from writing your own if you so desired. - - Extension Module - Ordinary Python modules are used to group related functionality together. - Taking inspiration from how Sphinx is architected, language servers are assembled by passing the list of modules to load to the :func:`~esbonio.lsp.create_language_server`. - This assembly process calls any functions with the name ``esbonio_setup`` allowing for ``LanguageFeatures`` to be configured and loaded into the server. - - Startup Module - As mentioned above, language servers are assembled and this is done inside a startup module. - A startup module in Esbonio is any Python script or module runnable by a ``python -m `` command that results in a running language server. - A good use case for a custom entry point would be starting up a language server instance pre configured with all the extensions required by your project. - - -.. _pygls: https://pygls.readthedocs.io/en/latest/index.html +Coming soon\ :sup:`TM` diff --git a/docs/lsp/extending/api-reference.rst b/docs/lsp/extending/api-reference.rst deleted file mode 100644 index 25314b70a..000000000 --- a/docs/lsp/extending/api-reference.rst +++ /dev/null @@ -1,71 +0,0 @@ -API Reference -============= - -.. warning:: - - While we will try not to break the API outlined below, until the language server - reaches ``v1.0`` we do not offer any stability guarantees. - -Language Servers ----------------- - -.. autofunction:: esbonio.lsp.create_language_server - -RstLanguageServer -^^^^^^^^^^^^^^^^^ - -.. autoclass:: esbonio.lsp.rst.RstLanguageServer - :members: - :show-inheritance: - -.. autoclass:: esbonio.lsp.rst.InitializationOptions - :members: - -.. autoclass:: esbonio.lsp.rst.config.ServerConfig - :members: - -.. autoclass:: esbonio.lsp.rst.config.ServerCompletionConfig - :members: - - -SphinxLanguageServer -^^^^^^^^^^^^^^^^^^^^ - -.. currentmodule:: esbonio.lsp.sphinx - -.. autoclass:: SphinxLanguageServer - :members: - :show-inheritance: - -.. autoclass:: InitializationOptions - :members: - -.. autoclass:: SphinxServerConfig - :members: - -.. autoclass:: SphinxConfig - :members: - -.. autoclass:: MissingConfigError - -Language Features ------------------ - -.. autoclass:: esbonio.lsp.LanguageFeature - :members: - -.. autoclass:: esbonio.lsp.CompletionContext - :members: - -.. autoclass:: esbonio.lsp.DefinitionContext - :members: - -.. autoclass:: esbonio.lsp.DocumentLinkContext - :members: - - -Testing -------- - -.. automodule:: esbonio.lsp.testing - :members: diff --git a/docs/lsp/extending/directives.rst b/docs/lsp/extending/directives.rst deleted file mode 100644 index 253e164cc..000000000 --- a/docs/lsp/extending/directives.rst +++ /dev/null @@ -1,48 +0,0 @@ -Directives -========== - -How To Guides -------------- - -The following guides outline how to extend the language server to add support for your custom directives. - -.. toctree:: - :glob: - :maxdepth: 1 - - directives/* - -API Reference -------------- - -.. currentmodule:: esbonio.lsp.directives - -.. autoclass:: Directives - :members: add_argument_completion_provider, - add_argument_definition_provider, - add_argument_link_provider, - add_documentation, - add_feature, - get_directives, - get_documentation, - get_implementation, - suggest_directives, - suggest_options - -.. autoclass:: DirectiveLanguageFeature - :members: - -.. autodata:: esbonio.lsp.util.patterns.DIRECTIVE - :no-value: - -.. autodata:: esbonio.lsp.util.patterns.DIRECTIVE_OPTION - :no-value: - -.. autoclass:: ArgumentCompletion - :members: - -.. autoclass:: ArgumentDefinition - :members: - -.. autoclass:: ArgumentLink - :members: diff --git a/docs/lsp/extending/directives/directive-registry.rst b/docs/lsp/extending/directives/directive-registry.rst deleted file mode 100644 index d74a490db..000000000 --- a/docs/lsp/extending/directives/directive-registry.rst +++ /dev/null @@ -1,122 +0,0 @@ -Supporting Custom Directive Registries -====================================== - -.. currentmodule:: esbonio.lsp.directives - -This guide walks through the process of teaching the language server how to discover directives stored in a custom registry. -Once complete, the following LSP features should start working with your directives. - -- Basic directive completions i.e. ``.. directive-name::`` but no argument completions. -- Basic option key completions i.e. ``:option-name:`` assuming options are declared in a directive's ``option_spec``, but no option value completions. -- Documentation hovers assuming you've provided documentation. -- Goto Implementation. - -.. note:: - - You may not need this guide. - - If you're registering your directive directly with - `docutils `__ or - `sphinx `__, - or using a `custom domain `__ - then you should find that the language server already has basic support for your custom directives out of the box. - - This guide is indended for adding support for directives that are not registered in a standard location. - -Still here? Great! Let's get started. - -Indexing Directives -------------------- - -As an example, we'll walk through the steps required to add (basic) support for Sphinx domains to the language server. - -.. note:: - - For the sake of brevity, some details have been omitted from the code examples below. - - If you're interested, you can find the actual implementation of the ``DomainDirectives`` class - `here `__. - -So that the server can discover the available directives, we have to provide a :class:`DirectiveLanguageFeature` that implements the :meth:`~DirectiveLanguageFeature.index_directives` method. -This method should return a dictionary where the keys are the canonical name of a directive which map to the class that implements it:: - - class DomainDirectives(DirectiveLanguageFeature): - def __init__(self, app: Sphinx): - self.app = app # Sphinx application instance. - - def index_directives(self) -> Dict[str, Type[Directive]]: - directives = {} - for prefix, domain in self.app.domains.items(): - for name, directive in domain.directives.items(): - directives[f"{prefix}:{name}"] = directive - - return directives - -In the case of Sphinx domains a directive's canonical name is of the form ``:`` e.g. ``py:function`` or ``c:macro``. - -This is the bare minimum required to make the language server aware of your custom directives, in fact if you were to try the above implementation you would already find completions being offered for domain based directives. -However, you would also notice that the short form of directives (e.g. ``function``) in the :ref:`standard ` and :confval:`primary ` domains are not included in the list of completions - despite being valid. - -To remedy this, you might be tempted to start adding multiple entries to the dictionary, one for each valid name **do not do this.** -Instead you can implement the :meth:`~DirectiveLanguageFeature.suggest_directives` method which solves this exact use case. - -.. tip:: - - If you want to play around with your own version of the ``DomainDirectives`` class you can disable the built in version by: - - - Passing the ``--exclude esbonio.lsp.sphinx.domains`` cli option, or - - If you're using VSCode adding ``esbonio.lsp.sphinx.domains`` to the :confval:`esbonio.server.excludedModules (string[])` option. - -(Optional) Suggesting Directives --------------------------------- - -The :meth:`~DirectiveLanguageFeature.suggest_directives` method is called each time the server is generating directive completions. -It can be used to tailor the list of directives that are offered to the user, depending on the current context. -Each ``DirectiveLanguageFeature`` has a default implementation, which may be sufficient depending on your use case:: - - def suggest_directives(self, context: CompletionContext) -> Iterable[Tuple[str, Type[Directive]]]: - return self.index_directives().items() - -However, in the case of Sphinx domains, we need to modify this to also include the short form of the directives in the standard and primary domains:: - - def suggest_directives(self, context: CompletionContext) -> Iterable[Tuple[str, Type[Directive]]]: - directives = self.index_directives() - primary_domain = self.app.config.primary_domain - - for key, directive in directives.items(): - - if key.startswith("std:"): - directives[key.replace("std:", "")] = directive - - if primary_domain and key.startswith(f"{primary_domain}:"): - directives[key.replace(f"{primary_domain}:", "")] = directive - - return directives.items() - -Now if you were to try this version, the short forms of the relevant directives would be offered as completion suggestions, but you would also notice that features like documentation hovers still don't work. -This is due to the language server not knowing which class implements these short form directives. - -(Optional) Implementation Lookups ---------------------------------- - -The :meth:`~DirectiveLanguageFeature.get_implementation` method is used by the language server to take a directive's name and lookup its implementation. -This powers features such as documentation hovers and goto implementation. -As with ``suggest_directives``, each ``DirectiveLanguageFeature`` has a default implementation which may be sufficient for your use case:: - - def get_implementation(self, directive: str, domain: Optional[str]) -> Optional[Type[Directive]]: - return self.index_directives().get(directive, None) - -In the case of Sphinx domains, if we see a directive without a domain prefix we need to see if it belongs to the standard or primary domains:: - - def get_implementation(self, directive: str, domain: Optional[str]) -> Optional[Type[Directive]]: - directives = self.index_directives() - - if domain is not None: - return directives.get(f"{domain}:{directive}", None) - - primary_domain = self.app.config.primary_domain - impl = directives.get(f"{primary_domain}:{directive}", None) - if impl is not None: - return impl - - return directives.get(f"std:{directive}", None) diff --git a/docs/lsp/extending/roles.rst b/docs/lsp/extending/roles.rst deleted file mode 100644 index 9253104ac..000000000 --- a/docs/lsp/extending/roles.rst +++ /dev/null @@ -1,49 +0,0 @@ -Roles -===== - -How To Guides -------------- - -The following guides outlne how to extens the language server to add support for your custom roles. - -.. toctree:: - :glob: - :maxdepth: 1 - - roles/* - -API Reference -------------- - -.. currentmodule:: esbonio.lsp.roles - -.. autoclass:: Roles - :members: add_documentation, - add_feature, - add_target_completion_provider, - add_target_definition_provider, - add_target_link_provider, - get_documentation, - get_implementation, - get_roles, - resolve_target_link, - suggest_roles, - suggest_targets - -.. autoclass:: RoleLanguageFeature - :members: - -.. autodata:: esbonio.lsp.util.patterns.ROLE - :no-value: - -.. autodata:: esbonio.lsp.util.patterns.DEFAULT_ROLE - :no-value: - -.. autoclass:: TargetDefinition - :members: - -.. autoclass:: TargetCompletion - :members: - -.. autoclass:: TargetLink - :members: diff --git a/docs/lsp/extending/roles/role-registry.rst b/docs/lsp/extending/roles/role-registry.rst deleted file mode 100644 index 12ae2e658..000000000 --- a/docs/lsp/extending/roles/role-registry.rst +++ /dev/null @@ -1,123 +0,0 @@ -Supporting Custom Role Registries -================================= - -.. currentmodule:: esbonio.lsp.roles - -This guide walks through the process of teaching the language server how to discover roles stored in a custom registry. -Once complete, the following LSP features should start working with your roles. - -- Basic role completions i.e. ``:role-name:`` but no target completions. -- Documentation hovers (assuming you've provided documentation) -- Goto Implementation - -.. note:: - - You may not need this guide. - - If you're registering your role directly with - `docutils `__ or - `sphinx `__, - or using a `custom domain `__ - then you should find that the language server already has basic support for your custom roles out of the box. - - This guide is indended for adding support for roles that are not registered in a standard location. - -Still here? Great! Let's get started. - -Indexing Roles --------------- - -As an example, we'll walk through the steps required to add (basic) support for Sphinx domains to the language server. - -.. note:: - - For the sake of brevity, some details have been omitted from the code examples below. - - If you're interested, you can find the actual implementation of the ``DomainRoles`` class - `here `__. - -So that the server can discover the available roles, we have to provide a :class:`RoleLanguageFeature` that implements the :meth:`~RoleLanguageFeature.index_roles` method. -This method should return a dictionary where the keys are the canonical name of the role which map to the function that implements it:: - - class DomainRoles(RoleLanguageFeature): - def __init__(self, app: Sphinx): - self.app = app # Sphinx application instance. - - def index_roles(self) -> Dict[str, Any]: - roles = {} - for prefix, domain in self.app.domains.items(): - for name, role in domain.roles.items(): - roles[f"{prefix}:{name}"] = role - - return roles - -In the case of Sphinx domains a role's canonical name is of the form ``:`` e.g. ``py:func`` or ``c:macro``. - -This is the bare minimum required to make the language server aware of your custom roles, in fact if you were to try the above implementation you would already find completions being offered for domain based roles. -However, you would also notice that the short form of roles (e.g. ``func``) in the :ref:`standard ` and :confval:`primary ` domains are not included in the list of completions - despite being valid. - -To remedy this, you might be tempted to start adding multiple entries to the dictionary, one for each valid name **do not do this.** -Instead you can implement the :meth:`~RoleLanguageFeature.suggest_roles` method which solves this exact use case. - -.. tip:: - - If you want to play around with your own version of the ``DomainRoles`` class you can disable the built in version by: - - - Passing the ``--exclude esbonio.lsp.sphinx.domains`` cli option, or - - If you're using VSCode adding ``esbonio.lsp.sphinx.domains`` to the :confval:`esbonio.server.excludedModules (string[])` option. - -(Optional) Suggesting Roles ---------------------------- - -The :meth:`~RoleLanguageFeature.suggest_roles` method is called each time the server is generating role completions. -It can be used to tailor the list of roles that are offered to the user, depending on the current context. -Each ``RoleLanguageFeature`` has a default implementation, which may be sufficient depending on your use case:: - - def suggest_roles(self, context: CompletionContext) -> Iterable[Tuple[str, Any]]: - """Suggest roles that may be used, given a completion context.""" - return self.index_roles().items() - -However, in the case of Sphinx domains, we need to modify this to also include the short form of the roles in the standard and primary domains:: - - def suggest_roles(self, context: CompletionContext) -> Iterable[Tuple[str, Any]]: - roles = self.index_roles() - primary_domain = self.app.config.primary_domain - - for key, role in roles.items(): - - if key.startswith("std:"): - roles[key.replace("std:", "")] = role - - if primary_domain and key.startswith(f"{primary_domain}:"): - roles[key.replace(f"{primary_domain}:", "")] = role - - return roles.items() - -Now if you were to try this version, the short forms of the relevant directives would be offered as completion suggestions, but you would also notice that features like documentation hovers still don't work. -This is due to the language server not knowing which class implements these short form directives. - -(Optional) Implementation Lookups ---------------------------------- - -The :meth:`~RoleLanguageFeature.get_implementation` method is used by the language server to take a role's name and lookup its implementation. -This powers features such as documentation hovers and goto implementation. -As with ``suggest_roles``, each ``RoleLanguageFeature`` has a default implementation which may be sufficient for your use case:: - - def get_implementation(self, role: str, domain: Optional[str]) -> Optional[Any]: - """Return the implementation for the given role name.""" - return self.index_roles().get(role, None) - -In the case of Sphinx domains, if we see a directive without a domain prefix we need to see if it belongs to the standard or primary domains:: - - def get_implementation(self, role: str, domain: Optional[str]) -> Optional[Any]: - roles = self.index_roles() - - if domain is not None: - return roles.get(f"{domain}:{role}", None) - - primary_domain = self.app.config.primary_domain - impl = roles.get(f"{primary_domain}:{role}", None) - if impl is not None: - return impl - - return roles.get(f"std:{role}", None)