diff options
Diffstat (limited to 'src/debputy/lsp/lsp_generic_deb822.py')
-rw-r--r-- | src/debputy/lsp/lsp_generic_deb822.py | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/src/debputy/lsp/lsp_generic_deb822.py b/src/debputy/lsp/lsp_generic_deb822.py new file mode 100644 index 0000000..245f3de --- /dev/null +++ b/src/debputy/lsp/lsp_generic_deb822.py @@ -0,0 +1,221 @@ +import re +from typing import ( + Optional, + Union, + Sequence, + Tuple, + Set, + Any, + Container, + List, +) + +from lsprotocol.types import ( + CompletionParams, + CompletionList, + CompletionItem, + Position, + CompletionItemTag, + MarkupContent, + Hover, + MarkupKind, + HoverParams, +) + +from debputy.lsp.lsp_debian_control_reference_data import ( + Deb822FileMetadata, + Deb822KnownField, + StanzaMetadata, +) +from debputy.lsp.text_util import normalize_dctrl_field_name +from debputy.util import _info + +try: + from pygls.server import LanguageServer + from pygls.workspace import TextDocument +except ImportError: + pass + + +_CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]") + + +def _at_cursor( + doc: "TextDocument", + lines: List[str], + client_position: Position, +) -> Tuple[Optional[str], str, bool, int, Set[str]]: + paragraph_no = -1 + paragraph_started = False + seen_fields = set() + last_field_seen: Optional[str] = None + current_field: Optional[str] = None + server_position = doc.position_codec.position_from_client_units( + lines, + client_position, + ) + position_line_no = server_position.line + + line_at_position = lines[position_line_no] + line_start = "" + if server_position.character: + line_start = line_at_position[0 : server_position.character] + + for line_no, line in enumerate(lines): + if not line or line.isspace(): + if line_no == position_line_no: + current_field = last_field_seen + continue + last_field_seen = None + if line_no > position_line_no: + break + paragraph_started = False + elif line and line[0] == "#": + continue + elif line and not line[0].isspace() and ":" in line: + if not paragraph_started: + paragraph_started = True + seen_fields = set() + paragraph_no += 1 + key, _ = line.split(":", 1) + key_lc = key.lower() + last_field_seen = key_lc + if line_no == position_line_no: + current_field = key_lc + seen_fields.add(key_lc) + + in_value = bool(_CONTAINS_SPACE_OR_COLON.search(line_start)) + current_word = doc.word_at_position(client_position) + if current_field is not None: + current_field = normalize_dctrl_field_name(current_field) + return current_field, current_word, in_value, paragraph_no, seen_fields + + +def deb822_completer( + ls: "LanguageServer", + params: CompletionParams, + file_metadata: Deb822FileMetadata[Any], +) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: + doc = ls.workspace.get_text_document(params.text_document.uri) + lines = doc.lines + + current_field, _, in_value, paragraph_no, seen_fields = _at_cursor( + doc, + lines, + params.position, + ) + + stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no) + + if in_value: + _info(f"Completion for field value {current_field}") + if current_field is None: + return None + known_field = stanza_metadata.get(current_field) + if known_field is None: + return None + items = _complete_field_value(known_field) + else: + _info("Completing field name") + items = _complete_field_name( + stanza_metadata, + seen_fields, + ) + + _info(f"Completion candidates: {items}") + + return items + + +def deb822_hover( + ls: "LanguageServer", + params: HoverParams, + file_metadata: Deb822FileMetadata[Any], +) -> Optional[Hover]: + doc = ls.workspace.get_text_document(params.text_document.uri) + lines = doc.lines + current_field, word_at_position, in_value, paragraph_no, _ = _at_cursor( + doc, lines, params.position + ) + stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no) + + if current_field is None: + _info("No hover information as we cannot determine which field it is for") + return None + known_field = stanza_metadata.get(current_field) + + if known_field is None: + return None + if in_value: + if not known_field.known_values: + return + keyword = known_field.known_values.get(word_at_position) + if keyword is None: + return + hover_text = keyword.hover_text + else: + hover_text = known_field.hover_text + if hover_text is None: + hover_text = f"The field {current_field} had no documentation." + + try: + supported_formats = ls.client_capabilities.text_document.hover.content_format + except AttributeError: + supported_formats = [] + + _info(f"Supported formats {supported_formats}") + markup_kind = MarkupKind.Markdown + if markup_kind not in supported_formats: + markup_kind = MarkupKind.PlainText + return Hover( + contents=MarkupContent( + kind=markup_kind, + value=hover_text, + ) + ) + + +def _should_complete_field_with_value(cand: Deb822KnownField) -> bool: + return cand.known_values is not None and ( + len(cand.known_values) == 1 + or ( + len(cand.known_values) == 2 + and cand.warn_if_default + and cand.default_value is not None + ) + ) + + +def _complete_field_name( + fields: StanzaMetadata[Any], + seen_fields: Container[str], +) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: + items = [] + for cand_key, cand in fields.items(): + if cand_key.lower() in seen_fields: + continue + name = cand.name + complete_as = name + ": " + if _should_complete_field_with_value(cand): + value = next(iter(v for v in cand.known_values if v != cand.default_value)) + complete_as += value + tags = [] + if cand.replaced_by or cand.deprecated_with_no_replacement: + tags.append(CompletionItemTag.Deprecated) + + items.append( + CompletionItem( + name, + insert_text=complete_as, + tags=tags, + ) + ) + return items + + +def _complete_field_value( + field: Deb822KnownField, +) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: + if field.known_values is None: + return None + return [CompletionItem(v) for v in field.known_values] |