usse/scrape/venv/lib/python3.10/site-packages/esbonio/lsp/directives/completions.py
2023-12-22 15:26:01 +01:00

354 lines
10 KiB
Python

import re
from typing import Optional
from typing import Type
from docutils.parsers.rst import Directive
from lsprotocol.types import CompletionItem
from lsprotocol.types import CompletionItemKind
from lsprotocol.types import InsertTextFormat
from lsprotocol.types import Position
from lsprotocol.types import Range
from lsprotocol.types import TextEdit
from esbonio.lsp import CompletionContext
__all__ = ["render_directive_completion", "render_directive_option_completion"]
WORD = re.compile("[a-zA-Z]+")
def render_directive_completion(
context: CompletionContext,
name: str,
directive: Type[Directive],
) -> Optional[CompletionItem]:
"""Render the given directive as a ``CompletionItem`` according to the current
context.
Parameters
----------
context
The context in which the completion should be rendered.
name
The name of the directive, as it appears in an rst file.
directive
The class that implements the directive.
Returns
-------
Optional[CompletionItem]
The final completion item or ``None``.
If ``None`` is returned, then the given completion should be skipped.
"""
if context.config.preferred_insert_behavior == "insert":
return _render_directive_with_insert_text(context, name, directive)
return _render_directive_with_text_edit(context, name, directive)
def render_directive_option_completion(
context: CompletionContext,
name: str,
directive: str,
implementation: Type[Directive],
) -> Optional[CompletionItem]:
"""Render the given directive option as a ``CompletionItem`` according to the
current context.
Parameters
----------
context
The context in which the completion should be rendered.
name
The name of the option, as it appears in an rst file.
directive
The name of the directive, as it appears in an rst file.
implementation
The class implementing the directive.
Returns
-------
Optional[CompletionItem]
The final completion item or ``None``.
If ``None`` is returned, the given completion should be skipped.
"""
if context.config.preferred_insert_behavior == "insert":
return _render_directive_option_with_insert_text(
context, name, directive, implementation
)
return _render_directive_option_with_text_edit(
context, name, directive, implementation
)
def _render_directive_with_insert_text(
context: CompletionContext,
name: str,
directive: Type[Directive],
) -> Optional[CompletionItem]:
"""Render a ``CompletionItem`` using ``insertText`` fields.
This implements the ``insert`` behavior for directives.
Parameters
----------
context
The context in which the completion is being generated.
name
The name of the directive, as it appears in an rst file.
directive
The class implementing the directive.
"""
insert_text = f".. {name}::"
user_text = context.match.group(0).strip()
# Since we can't replace any existing text, it only makes sense
# to offer completions that ailgn with what the user has already written.
if not insert_text.startswith(user_text):
return None
# Except that's not entirely true... to quote the LSP spec. (emphasis added)
#
# > in the model the client should filter against what the user has already typed
# > **using the word boundary rules of the language** (e.g. resolving the word
# > under the cursor position). The reason for this mode is that it makes it
# > extremely easy for a server to implement a basic completion list and get it
# > filtered on the client.
#
# So in other words... if the cursor is inside a word, that entire word will be
# replaced with what we have in `insert_text` so we need to be able to do something
# like
# .. -> image::
# .. im -> image::
#
# .. -> code-block::
# .. cod -> code-block::
# .. code-bl -> block::
#
# .. -> c:function::
# .. c -> c:function::
# .. c: -> function::
# .. c:fun -> function::
#
# And since the client is free to interpret this how it likes, it's unlikely we'll
# be able to get this right in all cases for all clients. So for now this is going
# to target Kate's interpretation since it currently does not support ``text_edit``
# and it was the editor that prompted this to be implemented in the first place.
#
# See: https://github.com/swyddfa/esbonio/issues/471
# If the existing text ends with a delimiter, then we should simply remove the
# entire prefix
if user_text.endswith((":", "-", " ")):
start_index = len(user_text)
# Look for groups of word chars, replace text until the start of the final group
else:
start_indices = [m.start() for m in WORD.finditer(user_text)] or [
len(user_text)
]
start_index = max(start_indices)
item = _render_directive_common(name, directive)
item.insert_text = insert_text[start_index:]
return item
def _render_directive_with_text_edit(
context: CompletionContext,
name: str,
directive: Type[Directive],
) -> Optional[CompletionItem]:
"""Render a directive's ``CompletionItem`` using the ``textEdit`` field.
This implements the ``replace`` insert behavior for directives.
Parameters
----------
context
The context in which the completion is being generated.
name
The name of the directive, as it appears in an rst file.
directive
The class implementing the directive.
"""
match = context.match
# Calculate the range of text the CompletionItems should edit.
# If there is an existing argument to the directive, we should leave it untouched
# otherwise, edit the whole line to insert any required arguments.
start = match.span()[0] + match.group(0).find(".")
include_argument = context.snippet_support
end = match.span()[1]
if match.group("argument"):
include_argument = False
end = match.span()[0] + match.group(0).find("::") + 2
range_ = Range(
start=Position(line=context.position.line, character=start),
end=Position(line=context.position.line, character=end),
)
# TODO: Give better names to arguments based on what they represent.
if include_argument:
insert_format = InsertTextFormat.Snippet
nargs = getattr(directive, "required_arguments", 0)
args = " " + " ".join("${{{0}:arg{0}}}".format(i) for i in range(1, nargs + 1))
else:
args = ""
insert_format = InsertTextFormat.PlainText
insert_text = f".. {name}::{args}"
item = _render_directive_common(name, directive)
item.filter_text = insert_text
item.text_edit = TextEdit(range=range_, new_text=insert_text)
item.insert_text_format = insert_format
return item
def _render_directive_common(
name: str,
directive: Type[Directive],
) -> CompletionItem:
"""Render the common fields of a directive's completion item."""
try:
dotted_name = f"{directive.__module__}.{directive.__name__}"
except AttributeError:
dotted_name = f"{directive.__module__}.{directive.__class__.__name__}"
return CompletionItem(
label=name,
detail=dotted_name,
kind=CompletionItemKind.Class,
data={"completion_type": "directive"},
)
def _render_directive_option_with_insert_text(
context: CompletionContext,
name: str,
directive: str,
implementation: Type[Directive],
) -> Optional[CompletionItem]:
"""Render a directive option's ``CompletionItem`` using the ``insertText`` field.
This implements the ``insert`` insert behavior for directive options.
Parameters
----------
context
The context in which the completion is being generated.
name
The name of the directive option, as it appears in an rst file.
directive
The name of the directive, as it appears in an rst file.
implementation
The class implementing the directive.
"""
insert_text = f":{name}:"
user_text = context.match.group(0).strip()
if not insert_text.startswith(user_text):
return None
if user_text.endswith((":", "-", " ")):
start_index = len(user_text)
else:
start_indices = [m.start() for m in WORD.finditer(user_text)] or [
len(user_text)
]
start_index = max(start_indices)
item = _render_directive_option_common(name, directive, implementation)
item.insert_text = insert_text[start_index:]
return item
def _render_directive_option_with_text_edit(
context: CompletionContext,
name: str,
directive: str,
implementation: Type[Directive],
) -> CompletionItem:
"""Render a directive option's ``CompletionItem`` using the``textEdit`` field.
This implements the ``replace`` insert behavior for directive options.
Parameters
----------
context
The context in which the completion is being generated.
name
The name of the directive option, as it appears in an rst file.
directive
The name of the directive, as it appears in an rst file.
implementation
The class implementing the directive.
"""
match = context.match
groups = match.groupdict()
option = groups["option"]
start = match.span()[0] + match.group(0).find(option)
end = start + len(option)
range_ = Range(
start=Position(line=context.position.line, character=start),
end=Position(line=context.position.line, character=end),
)
insert_text = f":{name}:"
item = _render_directive_option_common(name, directive, implementation)
item.filter_text = insert_text
item.text_edit = TextEdit(range=range_, new_text=insert_text)
return item
def _render_directive_option_common(
name: str, directive: str, impl: Type[Directive]
) -> CompletionItem:
"""Render the common fields of a directive option's completion item."""
try:
impl_name = f"{impl.__module__}.{impl.__name__}"
except AttributeError:
impl_name = f"{impl.__module__}.{impl.__class__.__name__}"
return CompletionItem(
label=name,
detail=f"{impl_name}:{name}",
kind=CompletionItemKind.Field,
data={"completion_type": "directive_option", "for_directive": directive},
)