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