Skip to content

Commit

Permalink
lsp: Split directive support into multiple features
Browse files Browse the repository at this point in the history
Rather than try and have a single `Directives` language feature that
does all the work of discovering directives, suggesting completions
AND making it all work for both rst and myst, this commit breaks the
work up between multiple `LanguageFeatures`.

There is a backend `Directives` feature, that will provide an API that
the various frontend features can use.

Then there is the `RstDirectives` and `MystDirectives` features that
build on the backend to surface the relevant features for their
respective syntax. At the moment they are nearly identical however,
they are now free to diverge to better support their associated syntax.
  • Loading branch information
alcarney committed May 9, 2024
1 parent 5a74041 commit 90277c9
Show file tree
Hide file tree
Showing 7 changed files with 502 additions and 73 deletions.
2 changes: 2 additions & 0 deletions lib/esbonio/esbonio/server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ def main(argv: Optional[Sequence[str]] = None):
"esbonio.server.features.preview_manager",
"esbonio.server.features.directives",
"esbonio.server.features.roles",
"esbonio.server.features.rst.directives",
"esbonio.server.features.myst.directives",
"esbonio.server.features.sphinx_support.diagnostics",
"esbonio.server.features.sphinx_support.symbols",
"esbonio.server.features.sphinx_support.directives",
Expand Down
78 changes: 5 additions & 73 deletions lib/esbonio/esbonio/server/features/directives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@
import typing

import attrs
from lsprotocol import types

from esbonio import server
from esbonio.sphinx_agent.types import MYST_DIRECTIVE
from esbonio.sphinx_agent.types import RST_DIRECTIVE

from . import completion

if typing.TYPE_CHECKING:
from typing import Any
Expand Down Expand Up @@ -45,13 +40,16 @@ def suggest_directives(


class DirectiveFeature(server.LanguageFeature):
"""Support for directives."""
"""'Backend' support for directives.
It's this language feature's responsibility to provide an API that exposes the
information a "frontend" language feature may want.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self._providers: Dict[int, DirectiveProvider] = {}
self._insert_behavior = "replace"

def add_provider(self, provider: DirectiveProvider):
"""Register a directive provider.
Expand All @@ -63,72 +61,6 @@ def add_provider(self, provider: DirectiveProvider):
"""
self._providers[id(provider)] = provider

completion_triggers = [RST_DIRECTIVE, MYST_DIRECTIVE]

def initialized(self, params: types.InitializedParams):
"""Called once the initial handshake between client and server has finished."""
self.configuration.subscribe(
"esbonio.server.completion",
server.CompletionConfig,
self.update_configuration,
)

def update_configuration(
self, event: server.ConfigChangeEvent[server.CompletionConfig]
):
"""Called when the user's configuration is updated."""
self._insert_behavior = event.value.preferred_insert_behavior

async def completion(
self, context: server.CompletionContext
) -> Optional[List[types.CompletionItem]]:
"""Provide completion suggestions for directives."""

groups = context.match.groupdict()

# Are we completing a directive's options?
if "directive" not in groups:
return await self.complete_options(context)

# Don't offer completions for targets
if (groups["name"] or "").startswith("_"):
return None

# Are we completing the directive's argument?
directive_end = context.match.span()[0] + len(groups["directive"])
complete_directive = groups["directive"].endswith(("::", "}"))

if complete_directive and directive_end < context.position.character:
return await self.complete_arguments(context)

return await self.complete_directives(context)

async def complete_options(self, context: server.CompletionContext):
return None

async def complete_arguments(self, context: server.CompletionContext):
return None

async def complete_directives(
self, context: server.CompletionContext
) -> Optional[List[types.CompletionItem]]:
"""Return completion suggestions for the available directives."""

language = self.server.get_language_at(context.doc, context.position)
render_func = completion.get_directive_renderer(language, self._insert_behavior)
if render_func is None:
return None

items = []
for directive in await self.suggest_directives(context):
if (item := render_func(context, directive)) is not None:
items.append(item)

if len(items) > 0:
return items

return None

async def suggest_directives(
self, context: server.CompletionContext
) -> List[Directive]:
Expand Down
Empty file.
95 changes: 95 additions & 0 deletions lib/esbonio/esbonio/server/features/myst/directives.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from __future__ import annotations

import typing

from lsprotocol import types

from esbonio import server
from esbonio.server.features.directives import DirectiveFeature
from esbonio.server.features.directives import completion
from esbonio.sphinx_agent.types import MYST_DIRECTIVE

if typing.TYPE_CHECKING:
from typing import List
from typing import Optional


class MystDirectives(server.LanguageFeature):
"""A frontend to directives for MyST syntax."""

def __init__(self, directives: DirectiveFeature, *args, **kwargs):
super().__init__(*args, **kwargs)

self.directives = directives
self._insert_behavior = "replace"

completion_triggers = [MYST_DIRECTIVE]

def initialized(self, params: types.InitializedParams):
"""Called once the initial handshake between client and server has finished."""
self.configuration.subscribe(
"esbonio.server.completion",
server.CompletionConfig,
self.update_configuration,
)

def update_configuration(
self, event: server.ConfigChangeEvent[server.CompletionConfig]
):
"""Called when the user's configuration is updated."""
self._insert_behavior = event.value.preferred_insert_behavior

async def completion(
self, context: server.CompletionContext
) -> Optional[List[types.CompletionItem]]:
"""Provide completion suggestions for directives."""

groups = context.match.groupdict()

# Are we completing a directive's options?
if "directive" not in groups:
return await self.complete_options(context)

# Don't offer completions for targets
if (groups["name"] or "").startswith("_"):
return None

# Are we completing the directive's argument?
directive_end = context.match.span()[0] + len(groups["directive"])
complete_directive = groups["directive"].endswith("}")

if complete_directive and directive_end < context.position.character:
return await self.complete_arguments(context)

return await self.complete_directives(context)

async def complete_options(self, context: server.CompletionContext):
return None

async def complete_arguments(self, context: server.CompletionContext):
return None

async def complete_directives(
self, context: server.CompletionContext
) -> Optional[List[types.CompletionItem]]:
"""Return completion suggestions for the available directives."""

language = self.server.get_language_at(context.doc, context.position)
render_func = completion.get_directive_renderer(language, self._insert_behavior)
if render_func is None:
return None

items = []
for directive in await self.directives.suggest_directives(context):
if (item := render_func(context, directive)) is not None:
items.append(item)

if len(items) > 0:
return items

return None


def esbonio_setup(esbonio: server.EsbonioLanguageServer, directives: DirectiveFeature):
myst_directives = MystDirectives(directives, esbonio)
esbonio.add_feature(myst_directives)
Empty file.
Loading

0 comments on commit 90277c9

Please sign in to comment.