Skip to content

Commit

Permalink
lsp: Introduce DirectiveLanguageFeatures
Browse files Browse the repository at this point in the history
The `Directives` language feature should now be extended by registering
`DirectiveLanguageFeatures`.

The existing `ArgumentCompletion`, `ArgumentDefinition` and
`ArgumentLink` extension points have been deprecated and will be removed
in ``v1.0``
  • Loading branch information
alcarney committed Aug 24, 2022
1 parent 719b8d1 commit 34adcbd
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 26 deletions.
1 change: 1 addition & 0 deletions lib/esbonio/changes/443.api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ``add_diagnostics`` method to the ``RstLanguageServer`` to enable adding diagnostics to a document incrementally.
2 changes: 2 additions & 0 deletions lib/esbonio/changes/444.api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The ``Directives`` language feature can now be extended by registering ``DirectiveLanguageFeatures`` using the new ``add_feature`` method.
This is now the preferred extension mechanism and should be used by all extensions going forward.
1 change: 1 addition & 0 deletions lib/esbonio/changes/444.deprecated.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``ArgumentCompletion``, ``ArgumentDefinition`` and ``ArgumentLink`` directive providers have been deprecated in favour of ``DirectiveLanguageFeatures`` and will be removed in ``v1.0``
210 changes: 184 additions & 26 deletions lib/esbonio/esbonio/lsp/directives.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import re
import typing
import warnings
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union

from pygls.lsp.types import CompletionItem
from pygls.lsp.types import CompletionItemKind
Expand All @@ -31,8 +33,79 @@
from esbonio.lsp.util.patterns import DIRECTIVE_OPTION


class DirectiveLanguageFeature:
"""Base class for directive language features."""

def complete_arguments(
self, context: CompletionContext, domain: str, name: str
) -> List[CompletionItem]:
"""Return a list of completion items representing valid targets for the given
directive.
Parameters
----------
context:
The completion context
domain:
The name of the domain the directive is a member of
name:
The name of the domain
"""
return []

def resolve_link(
self,
context: DocumentLinkContext,
directive: str,
domain: Optional[str],
argument: str,
) -> Union[Tuple[str, str], str, None]:
"""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.
"""
return None

def find_definitions(
self,
context: DefinitionContext,
directive: str,
domain: Optional[str],
argument: str,
) -> List[Location]:
"""Return a list of locations representing definitions of the given argument.
Parameters
----------
context:
The context of the definition 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 find the definition of.
"""
return []


class ArgumentCompletion(Protocol):
"""A completion provider for directive arguments."""
"""A completion provider for directive arguments.
.. deprecated:: xxxx
This will be removed in ``v1.0``, use subclasses of
:class:`~esbonio.lsp.directives.DirectiveLanguageFeature` instead.
"""

def complete_arguments(
self, context: CompletionContext, domain: str, name: str
Expand All @@ -52,7 +125,13 @@ def complete_arguments(


class ArgumentDefinition(Protocol):
"""A definition provider for directive arguments."""
"""A definition provider for directive arguments.
.. deprecated:: xxxx
This will be removed in ``v1.0``, use subclasses of
:class:`~esbonio.lsp.directives.DirectiveLanguageFeature` instead.
"""

def find_definitions(
self,
Expand All @@ -77,7 +156,13 @@ def find_definitions(


class ArgumentLink(Protocol):
"""A document link resolver for directive arguments."""
"""A document link resolver for directive arguments.
.. deprecated:: xxxx
This will be removed in ``v1.0``, use subclasses of
:class:`~esbonio.lsp.directives.DirectiveLanguageFeature` instead.
"""

def resolve_link(
self,
Expand Down Expand Up @@ -110,49 +195,118 @@ def __init__(self, *args, **kwargs):
self._documentation: Dict[str, Dict[str, str]] = {}
"""Cache for documentation."""

self._argument_completion_providers: Dict[str, ArgumentCompletion] = {}
"""A dictionary of providers that give completion suggestions for directive
arguments."""
self._features: Dict[str, DirectiveLanguageFeature] = {}
"""The collection of registered features."""

self._argument_definition_providers: Dict[str, ArgumentDefinition] = {}
"""A dictionary of providers that locate definitions for directive arguments."""
def add_feature(self, feature: DirectiveLanguageFeature):
"""Register a directive language feature.
self._argument_link_providers: Dict[str, ArgumentLink] = {}
"""A dictionary of providers that resolve document links for directive
arguments."""
Parameters
----------
feature
The directive language feature
"""
key = f"{feature.__module__}.{feature.__class__.__name__}"
self._features[key] = feature

def add_argument_completion_provider(self, provider: ArgumentCompletion) -> None:
"""Register an :class:`~esbonio.lsp.directives.ArgumentCompletion` provider.
.. deprecated:: xxxx
This will be removed in ``v1.0``, use
:meth:`esbonio.lsp.directives.Directives.add_feature` with a
:class:`esbonio.lsp.directives.DirectiveLanguageFeature` subclass instead.
Parameters
----------
provider:
The provider to register.
"""
key = f"{provider.__module__}.{provider.__class__.__name__}"
self._argument_completion_providers[key] = provider
warnings.warn(
"ArgumentCompletion providers are deprecated in favour of "
"DirectiveLanguageFeatures, this method will be removed in v1.0",
DeprecationWarning,
stacklevel=2,
)

name = provider.__class__.__name__
key = f"{provider.__module__}.{name}.completion"

# Automatically derive the feature definition from the provider.
feature = type(
f"{name}CompletionProvider",
(DirectiveLanguageFeature,),
{"complete_arguments": provider.complete_arguments},
)()

self._features[key] = feature

def add_argument_definition_provider(self, provider: ArgumentDefinition) -> None:
"""Register an :class:`~esbonio.lsp.directives.ArgumentDefinition` provider.
.. deprecated:: xxxx
This will be removed in ``v1.0``, use
:meth:`esbonio.lsp.directives.Directives.add_feature` with a
:class:`esbonio.lsp.directives.DirectiveLanguageFeature` subclass instead.
Parameters
----------
provider:
The provider to register.
"""
key = f"{provider.__module__}.{provider.__class__.__name__}"
self._argument_definition_providers[key] = provider
warnings.warn(
"ArgumentDefinition providers are deprecated in favour of "
"DirectiveLanguageFeatures, this method will be removed in v1.0",
DeprecationWarning,
stacklevel=2,
)

name = provider.__class__.__name__
key = f"{provider.__module__}.{name}.definitions"

# Automatically derive the feature definition from the provider.
feature = type(
f"{name}DefinitionProvider",
(DirectiveLanguageFeature,),
{"find_definitions": provider.find_definitions},
)()

self._features[key] = feature

def add_argument_link_provider(self, provider: ArgumentLink) -> None:
"""Register an :class:`~esbonio.lsp.directives.ArgumentLink` provider.
.. deprecated:: xxxx
This will be removed in ``v1.0``, use
:meth:`esbonio.lsp.directives.Directives.add_feature` with a
:class:`esbonio.lsp.directives.DirectiveLanguageFeature` subclass instead.
Parameters
----------
provider:
The provider to register.
"""
key = f"{provider.__module__}.{provider.__class__.__name__}"
self._argument_link_providers[key] = provider
warnings.warn(
"ArgumentLink providers are deprecated in favour of "
"DirectiveLanguageFeatures, this method will be removed in v1.0",
DeprecationWarning,
stacklevel=2,
)

name = provider.__class__.__name__
key = f"{provider.__module__}.{name}.links"

# Automatically derive the feature definition from the provider.
feature = type(
f"{name}LinkProvider",
(DirectiveLanguageFeature,),
{"resolve_link": provider.resolve_link},
)()

self._features[key] = feature

def add_documentation(self, documentation: Dict[str, Dict[str, Any]]) -> None:
"""Register directive documentation.
Expand Down Expand Up @@ -280,8 +434,8 @@ def complete_arguments(self, context: CompletionContext) -> List[CompletionItem]
name = context.match.group("name")
domain = context.match.group("domain") or ""

for provider_name, provider in self._argument_completion_providers.items():
arguments += provider.complete_arguments(context, domain, name) or []
for feature in self._features.values():
arguments += feature.complete_arguments(context, domain, name) or []

return arguments

Expand Down Expand Up @@ -470,9 +624,9 @@ def find_argument_definition(
) -> List[Location]:
definitions = []

for _, provider in self._argument_definition_providers.items():
for feature in self._features.values():
definitions += (
provider.find_definitions(context, directive, domain, argument) or []
feature.find_definitions(context, directive, domain, argument) or []
)

return definitions
Expand All @@ -492,11 +646,15 @@ def document_link(self, context: DocumentLinkContext) -> List[DocumentLink]:

target = None
tooltip = None
for provider in self._argument_link_providers.values():
target, tooltip = provider.resolve_link(
context, name, domain, argument
)
if target:
for feature in self._features.values():
result = feature.resolve_link(context, name, domain, argument)

if isinstance(result, str):
target = result
break

if isinstance(result, tuple) and isinstance(result[0], str):
target, tooltip = result
break

if not target:
Expand Down

0 comments on commit 34adcbd

Please sign in to comment.