354 lines
10 KiB
Python
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},
|
|
)
|