Coverage for src/debputy/lsp/lsp_reference_keyword.py: 98%
56 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
1import dataclasses
2import textwrap
3from typing import (
4 Optional,
5 Union,
6 Any,
7 Self,
8 TYPE_CHECKING,
9)
10from collections.abc import Mapping, Sequence, Callable, Iterable
12from debputy.lsp.named_styles import ALL_PUBLIC_NAMED_STYLES
13from debputy.lsp.ref_models.deb822_reference_parse_models import UsageHint
14from debputy.lsp.vendoring._deb822_repro import Deb822ParagraphElement
16if TYPE_CHECKING:
17 import lsprotocol.types as types
18 from debputy.linting.lint_util import LintState
19 from debputy.lsp.debputy_ls import DebputyLanguageServer
20else:
21 import debputy.lsprotocol.types as types
23LSP_DATA_DOMAIN = "debputy-lsp-data"
26def format_comp_item_synopsis_doc(
27 usage_hint: UsageHint | None,
28 synopsis_doc: str | None,
29 is_deprecated: bool,
30) -> str:
31 if is_deprecated:
32 return (
33 f"[OBSOLETE]: {synopsis_doc}"
34 if synopsis_doc is not None and not synopsis_doc.isspace()
35 else f"[OBSOLETE]"
36 )
37 if usage_hint is not None:
38 return (
39 f"[{usage_hint.upper()}]: {synopsis_doc}"
40 if synopsis_doc is not None and not synopsis_doc.isspace()
41 else f"[{usage_hint.upper()}]"
42 )
43 return synopsis_doc
46@dataclasses.dataclass(slots=True, frozen=True)
47class Keyword:
48 value: str
49 synopsis: str | None = None
50 long_description: str | None = None
51 translation_context: str = ""
52 is_obsolete: bool = False
53 replaced_by: str | None = None
54 is_exclusive: bool = False
55 """For keywords in fields that allow multiple keywords, the `is_exclusive` can be
56 used for keywords that cannot be used with other keywords. As an example, the `all`
57 value in `Architecture` of `debian/control` cannot be used with any other architecture.
58 """
59 is_alias_of: str | None = None
60 is_completion_suggestion: bool = True
61 sort_text: None | (
62 str
63 | Callable[["Keyword", "LintState", Sequence[Deb822ParagraphElement], str], str]
64 ) = None
65 usage_hint: UsageHint | None = None
66 can_complete_keyword_in_stanza: None | (
67 Callable[[Iterable[Deb822ParagraphElement]], bool]
68 ) = None
70 @property
71 def is_deprecated(self) -> bool:
72 return self.is_obsolete or self.replaced_by is not None
74 def resolve_sort_text(
75 self,
76 lint_state: "LintState",
77 stanza_parts: Sequence[Deb822ParagraphElement],
78 value_being_completed: str,
79 ) -> str | None:
80 sort_text = self.sort_text
81 if sort_text is None:
82 return None
83 if not isinstance(sort_text, str):
84 return sort_text(
85 self,
86 lint_state,
87 stanza_parts,
88 value_being_completed,
89 )
90 return sort_text
92 def is_keyword_valid_completion_in_stanza(
93 self,
94 stanza_parts: Sequence[Deb822ParagraphElement],
95 ) -> bool:
96 return (
97 self.can_complete_keyword_in_stanza is None
98 or self.can_complete_keyword_in_stanza(stanza_parts)
99 )
101 def replace(self, **changes: Any) -> "Self":
102 return dataclasses.replace(self, **changes)
104 def synopsis_translated(
105 self, translation_provider: Union["DebputyLanguageServer", "LintState"]
106 ) -> str:
107 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext(
108 self.translation_context,
109 self.synopsis,
110 )
112 def long_description_translated(
113 self, translation_provider: Union["DebputyLanguageServer", "LintState"]
114 ) -> str:
115 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext(
116 self.translation_context,
117 self.long_description,
118 )
120 def as_completion_item(
121 self,
122 lint_state: "LintState",
123 stanza_parts: Sequence[Deb822ParagraphElement],
124 value_being_completed: str,
125 markdown_kind: types.MarkupKind,
126 ) -> types.CompletionItem:
127 return types.CompletionItem(
128 self.value,
129 insert_text=self.value if self.is_alias_of is None else self.is_alias_of,
130 sort_text=self.resolve_sort_text(
131 lint_state,
132 stanza_parts,
133 value_being_completed,
134 ),
135 detail=format_comp_item_synopsis_doc(
136 self.usage_hint,
137 self.synopsis_translated(lint_state),
138 self.is_deprecated,
139 ),
140 deprecated=self.is_deprecated,
141 tags=[types.CompletionItemTag.Deprecated] if self.is_deprecated else None,
142 documentation=(
143 types.MarkupContent(value=self.long_description, kind=markdown_kind)
144 if self.long_description
145 else None
146 ),
147 )
150def allowed_values(*values: str | Keyword) -> Mapping[str, Keyword]:
151 as_keywords = [k if isinstance(k, Keyword) else Keyword(k) for k in values]
152 as_mapping = {k.value: k for k in as_keywords if k.value}
153 # Simple bug check
154 assert len(as_keywords) == len(as_mapping)
155 return as_mapping
158# This is the set of styles that `debputy` explicitly supports, which is more narrow than
159# the ones in the config file.
160ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS = allowed_values(
161 Keyword(s.name, long_description=s.long_description)
162 for s in ALL_PUBLIC_NAMED_STYLES
163)