Coverage for src/debputy/lsp/languages/lsp_debian_watch.py: 86%
142 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 importlib.resources
3import re
4from functools import lru_cache
5from typing import (
6 Union,
7 Optional,
8 TYPE_CHECKING,
9 List,
10 Self,
11)
12from collections.abc import Sequence, Mapping
14from debputy.linting.lint_util import LintState, with_range_in_continuous_parts
15from debputy.lsp.debputy_ls import DebputyLanguageServer
16from debputy.lsp.lsp_debian_control_reference_data import (
17 DebianWatch5FileMetadata,
18 Deb822KnownField,
19)
21import debputy.lsp.data.deb822_data as deb822_ref_data_dir
22from debputy.lsp.lsp_features import (
23 lint_diagnostics,
24 lsp_completer,
25 lsp_hover,
26 lsp_standard_handler,
27 lsp_folding_ranges,
28 lsp_semantic_tokens_full,
29 lsp_will_save_wait_until,
30 lsp_format_document,
31 SecondaryLanguage,
32 LanguageDispatchRule,
33 lsp_cli_reformat_document,
34)
35from debputy.lsp.lsp_generic_deb822 import (
36 deb822_completer,
37 deb822_hover,
38 deb822_folding_ranges,
39 deb822_semantic_tokens_full,
40 deb822_format_file,
41 scan_for_syntax_errors_and_token_level_diagnostics,
42)
43from debputy.lsp.lsp_reference_keyword import LSP_DATA_DOMAIN
44from debputy.lsp.ref_models.deb822_reference_parse_models import (
45 GenericVariable,
46 GENERIC_VARIABLE_REFERENCE_DATA_PARSER,
47)
48from debputy.lsp.text_util import markdown_urlify
49from debputy.lsp.vendoring._deb822_repro import (
50 Deb822ParagraphElement,
51)
52from debputy.lsprotocol.types import (
53 CompletionItem,
54 CompletionList,
55 CompletionParams,
56 HoverParams,
57 Hover,
58 TEXT_DOCUMENT_CODE_ACTION,
59 SemanticTokens,
60 SemanticTokensParams,
61 FoldingRangeParams,
62 FoldingRange,
63 WillSaveTextDocumentParams,
64 TextEdit,
65 DocumentFormattingParams,
66)
67from debputy.manifest_parser.util import AttributePath
68from debputy.util import _info
69from debputy.yaml import MANIFEST_YAML
71try:
72 from debputy.lsp.vendoring._deb822_repro.locatable import (
73 Position as TEPosition,
74 Range as TERange,
75 )
77 from pygls.server import LanguageServer
78 from pygls.workspace import TextDocument
79except ImportError:
80 pass
83if TYPE_CHECKING:
84 import lsprotocol.types as types
85else:
86 import debputy.lsprotocol.types as types
89_CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]")
91_DISPATCH_RULE = LanguageDispatchRule.new_rule(
92 "debian/watch",
93 None,
94 "debian/watch",
95 [
96 # Presumably, emacs's name
97 SecondaryLanguage("debian-watch"),
98 # Presumably, vim's name
99 SecondaryLanguage("debwatch"),
100 ],
101)
103_DWATCH_FILE_METADATA = DebianWatch5FileMetadata()
105lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION)
108@dataclasses.dataclass(slots=True, frozen=True)
109class VariableMetadata:
110 name: str
111 doc_uris: Sequence[str]
112 synopsis: str
113 description: str
115 def render_metadata_fields(self) -> str:
116 doc_uris = self.doc_uris
117 parts = []
118 if doc_uris: 118 ↛ 124line 118 didn't jump to line 124 because the condition on line 118 was always true
119 if len(doc_uris) == 1: 119 ↛ 122line 119 didn't jump to line 122 because the condition on line 119 was always true
120 parts.append(f"Documentation: {markdown_urlify(doc_uris[0])}")
121 else:
122 parts.append("Documentation:")
123 parts.extend(f" - {markdown_urlify(uri)}" for uri in doc_uris)
124 return "\n".join(parts)
126 @classmethod
127 def from_ref_data(cls, x: GenericVariable) -> "Self":
128 doc = x.get("documentation", {})
129 return cls(
130 x["name"],
131 doc.get("uris", []),
132 doc.get("synopsis", ""),
133 doc.get("long_description", ""),
134 )
137def dwatch_variables_metadata_basename() -> str:
138 return "debian_watch_variables_data.yaml"
141def _as_variables_metadata(
142 args: list[VariableMetadata],
143) -> Mapping[str, VariableMetadata]:
144 r = {s.name: s for s in args}
145 assert len(r) == len(args)
146 return r
149@lru_cache
150def dwatch_variables_metadata() -> Mapping[str, VariableMetadata]:
151 p = importlib.resources.files(deb822_ref_data_dir.__name__).joinpath(
152 dwatch_variables_metadata_basename()
153 )
155 with p.open("r", encoding="utf-8") as fd:
156 raw = MANIFEST_YAML.load(fd)
158 attr_path = AttributePath.root_path(p)
159 ref = GENERIC_VARIABLE_REFERENCE_DATA_PARSER.parse_input(raw, attr_path)
160 return _as_variables_metadata(
161 [VariableMetadata.from_ref_data(x) for x in ref["variables"]]
162 )
165def _custom_hover(
166 ls: "DebputyLanguageServer",
167 server_position: types.Position,
168 _current_field: str | None,
169 _word_at_position: str,
170 _known_field: Deb822KnownField | None,
171 in_value: bool,
172 _doc: "TextDocument",
173 lines: list[str],
174) -> Hover | str | None:
175 if not in_value: 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true
176 return None
178 line_no = server_position.line
179 line = lines[line_no]
180 variable_search_ref = server_position.character
181 variable = ""
182 try:
183 # Unlike ${} substvars where the start and end uses distinct characters, we cannot
184 # know for certain whether we are at the start or end of a variable when we land
185 # directly on a separator.
186 try:
187 variable_start = line.rindex("@", 0, variable_search_ref)
188 except ValueError:
189 if line[variable_search_ref] != "@":
190 raise
191 variable_start = variable_search_ref
193 variable_end = line.index("@", variable_start + 1)
194 if server_position.character <= variable_end: 194 ↛ 199line 194 didn't jump to line 199 because the condition on line 194 was always true
195 variable = line[variable_start : variable_end + 1]
196 except (ValueError, IndexError):
197 pass
199 if variable != "" and variable != "@@":
200 substvar_md = dwatch_variables_metadata().get(variable)
202 if substvar_md is None: 202 ↛ 204line 202 didn't jump to line 204 because the condition on line 202 was never true
203 # In case of `@PACKAGE@-lin<CURSOR>ux-@ANY_VERSION@`
204 return None
205 doc = ls.translation(LSP_DATA_DOMAIN).pgettext(
206 f"Variable:{substvar_md.name}",
207 substvar_md.description,
208 )
209 md_fields = "\n" + substvar_md.render_metadata_fields()
210 return f"# Variable `{variable}`\n\n{doc}{md_fields}"
212 return None
215@lsp_hover(_DISPATCH_RULE)
216def _debian_watch_hover(
217 ls: "DebputyLanguageServer",
218 params: HoverParams,
219) -> Hover | None:
220 return deb822_hover(ls, params, _DWATCH_FILE_METADATA, custom_handler=_custom_hover)
223@lsp_completer(_DISPATCH_RULE)
224def _debian_watch_completions(
225 ls: "DebputyLanguageServer",
226 params: CompletionParams,
227) -> CompletionList | Sequence[CompletionItem] | None:
228 return deb822_completer(ls, params, _DWATCH_FILE_METADATA)
231@lsp_folding_ranges(_DISPATCH_RULE)
232def _debian_watch_folding_ranges(
233 ls: "DebputyLanguageServer",
234 params: FoldingRangeParams,
235) -> Sequence[FoldingRange] | None:
236 return deb822_folding_ranges(ls, params, _DWATCH_FILE_METADATA)
239@lint_diagnostics(_DISPATCH_RULE)
240async def _lint_debian_watch(lint_state: LintState) -> None:
241 deb822_file = lint_state.parsed_deb822_file_content
243 if not _DWATCH_FILE_METADATA.file_metadata_applies_to_file(deb822_file): 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true
244 return
246 first_error = await scan_for_syntax_errors_and_token_level_diagnostics(
247 deb822_file,
248 lint_state,
249 )
250 header_stanza, source_stanza = _DWATCH_FILE_METADATA.stanza_types()
251 stanza_no = 0
253 async for stanza_range, stanza in lint_state.slow_iter(
254 with_range_in_continuous_parts(deb822_file.iter_parts())
255 ):
256 if not isinstance(stanza, Deb822ParagraphElement):
257 continue
258 stanza_position = stanza_range.start_pos
259 if stanza_position.line_position >= first_error: 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true
260 break
261 stanza_no += 1
262 is_source_stanza = stanza_no != 1
263 if is_source_stanza:
264 stanza_metadata = _DWATCH_FILE_METADATA.classify_stanza(
265 stanza,
266 stanza_no,
267 )
268 other_stanza_metadata = header_stanza
269 other_stanza_name = "Header"
270 elif "Version" in stanza: 270 ↛ 275line 270 didn't jump to line 275 because the condition on line 270 was always true
271 stanza_metadata = header_stanza
272 other_stanza_metadata = source_stanza
273 other_stanza_name = "Source"
274 else:
275 break
277 await stanza_metadata.stanza_diagnostics(
278 deb822_file,
279 stanza,
280 stanza_position,
281 lint_state,
282 confusable_with_stanza_name=other_stanza_name,
283 confusable_with_stanza_metadata=other_stanza_metadata,
284 )
287@lsp_will_save_wait_until(_DISPATCH_RULE)
288def _debian_watch_on_save_formatting(
289 ls: "DebputyLanguageServer",
290 params: WillSaveTextDocumentParams,
291) -> Sequence[TextEdit] | None:
292 doc = ls.workspace.get_text_document(params.text_document.uri)
293 lint_state = ls.lint_state(doc)
294 return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)
297@lsp_cli_reformat_document(_DISPATCH_RULE)
298def _reformat_debian_watch(
299 lint_state: LintState,
300) -> Sequence[TextEdit] | None:
301 return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)
304@lsp_format_document(_DISPATCH_RULE)
305def _debian_watch_format_doc(
306 ls: "DebputyLanguageServer",
307 params: DocumentFormattingParams,
308) -> Sequence[TextEdit] | None:
309 doc = ls.workspace.get_text_document(params.text_document.uri)
310 lint_state = ls.lint_state(doc)
311 return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)
314@lsp_semantic_tokens_full(_DISPATCH_RULE)
315async def _debian_watch_semantic_tokens_full(
316 ls: "DebputyLanguageServer",
317 request: SemanticTokensParams,
318) -> SemanticTokens | None:
319 return await deb822_semantic_tokens_full(
320 ls,
321 request,
322 _DWATCH_FILE_METADATA,
323 )