summaryrefslogtreecommitdiffstats
path: root/src/debputy/lsp/lsp_generic_deb822.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/lsp/lsp_generic_deb822.py')
-rw-r--r--src/debputy/lsp/lsp_generic_deb822.py221
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]