Coverage for src/debputy/lsp/languages/lsp_debian_watch.py: 86%
141 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-02-28 21:56 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-02-28 21:56 +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 debian._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.yaml import MANIFEST_YAML
70try:
71 from debian._deb822_repro.locatable import (
72 Position as TEPosition,
73 Range as TERange,
74 )
76 from pygls.server import LanguageServer
77 from pygls.workspace import TextDocument
78except ImportError:
79 pass
82if TYPE_CHECKING:
83 import lsprotocol.types as types
84else:
85 import debputy.lsprotocol.types as types
88_CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]")
90_DISPATCH_RULE = LanguageDispatchRule.new_rule(
91 "debian/watch",
92 None,
93 "debian/watch",
94 [
95 # Presumably, emacs's name
96 SecondaryLanguage("debian-watch"),
97 # Presumably, vim's name
98 SecondaryLanguage("debwatch"),
99 ],
100)
102_DWATCH_FILE_METADATA = DebianWatch5FileMetadata()
104lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION)
107@dataclasses.dataclass(slots=True, frozen=True)
108class VariableMetadata:
109 name: str
110 doc_uris: Sequence[str]
111 synopsis: str
112 description: str
114 def render_metadata_fields(self) -> str:
115 doc_uris = self.doc_uris
116 parts = []
117 if doc_uris: 117 ↛ 123line 117 didn't jump to line 123 because the condition on line 117 was always true
118 if len(doc_uris) == 1: 118 ↛ 121line 118 didn't jump to line 121 because the condition on line 118 was always true
119 parts.append(f"Documentation: {markdown_urlify(doc_uris[0])}")
120 else:
121 parts.append("Documentation:")
122 parts.extend(f" - {markdown_urlify(uri)}" for uri in doc_uris)
123 return "\n".join(parts)
125 @classmethod
126 def from_ref_data(cls, x: GenericVariable) -> "Self":
127 doc = x.get("documentation", {})
128 return cls(
129 x["name"],
130 doc.get("uris", []),
131 doc.get("synopsis", ""),
132 doc.get("long_description", ""),
133 )
136def dwatch_variables_metadata_basename() -> str:
137 return "debian_watch_variables_data.yaml"
140def _as_variables_metadata(
141 args: list[VariableMetadata],
142) -> Mapping[str, VariableMetadata]:
143 r = {s.name: s for s in args}
144 assert len(r) == len(args)
145 return r
148@lru_cache
149def dwatch_variables_metadata() -> Mapping[str, VariableMetadata]:
150 p = importlib.resources.files(deb822_ref_data_dir.__name__).joinpath(
151 dwatch_variables_metadata_basename()
152 )
154 with p.open("r", encoding="utf-8") as fd:
155 raw = MANIFEST_YAML.load(fd)
157 attr_path = AttributePath.root_path(p)
158 ref = GENERIC_VARIABLE_REFERENCE_DATA_PARSER.parse_input(raw, attr_path)
159 return _as_variables_metadata(
160 [VariableMetadata.from_ref_data(x) for x in ref["variables"]]
161 )
164def _custom_hover(
165 ls: "DebputyLanguageServer",
166 server_position: types.Position,
167 _current_field: str | None,
168 _word_at_position: str,
169 _known_field: Deb822KnownField | None,
170 in_value: bool,
171 _doc: "TextDocument",
172 lines: list[str],
173) -> Hover | str | None:
174 if not in_value: 174 ↛ 175line 174 didn't jump to line 175 because the condition on line 174 was never true
175 return None
177 line_no = server_position.line
178 line = lines[line_no]
179 variable_search_ref = server_position.character
180 variable = ""
181 try:
182 # Unlike ${} substvars where the start and end uses distinct characters, we cannot
183 # know for certain whether we are at the start or end of a variable when we land
184 # directly on a separator.
185 try:
186 variable_start = line.rindex("@", 0, variable_search_ref)
187 except ValueError:
188 if line[variable_search_ref] != "@":
189 raise
190 variable_start = variable_search_ref
192 variable_end = line.index("@", variable_start + 1)
193 if server_position.character <= variable_end: 193 ↛ 198line 193 didn't jump to line 198 because the condition on line 193 was always true
194 variable = line[variable_start : variable_end + 1]
195 except (ValueError, IndexError):
196 pass
198 if variable != "" and variable != "@@":
199 substvar_md = dwatch_variables_metadata().get(variable)
201 if substvar_md is None: 201 ↛ 203line 201 didn't jump to line 203 because the condition on line 201 was never true
202 # In case of `@PACKAGE@-lin<CURSOR>ux-@ANY_VERSION@`
203 return None
204 doc = ls.translation(LSP_DATA_DOMAIN).pgettext(
205 f"Variable:{substvar_md.name}",
206 substvar_md.description,
207 )
208 md_fields = "\n" + substvar_md.render_metadata_fields()
209 return f"# Variable `{variable}`\n\n{doc}{md_fields}"
211 return None
214@lsp_hover(_DISPATCH_RULE)
215def _debian_watch_hover(
216 ls: "DebputyLanguageServer",
217 params: HoverParams,
218) -> Hover | None:
219 return deb822_hover(ls, params, _DWATCH_FILE_METADATA, custom_handler=_custom_hover)
222@lsp_completer(_DISPATCH_RULE)
223def _debian_watch_completions(
224 ls: "DebputyLanguageServer",
225 params: CompletionParams,
226) -> CompletionList | Sequence[CompletionItem] | None:
227 return deb822_completer(ls, params, _DWATCH_FILE_METADATA)
230@lsp_folding_ranges(_DISPATCH_RULE)
231def _debian_watch_folding_ranges(
232 ls: "DebputyLanguageServer",
233 params: FoldingRangeParams,
234) -> Sequence[FoldingRange] | None:
235 return deb822_folding_ranges(ls, params, _DWATCH_FILE_METADATA)
238@lint_diagnostics(_DISPATCH_RULE)
239async def _lint_debian_watch(lint_state: LintState) -> None:
240 deb822_file = lint_state.parsed_deb822_file_content
242 if not _DWATCH_FILE_METADATA.file_metadata_applies_to_file(deb822_file): 242 ↛ 243line 242 didn't jump to line 243 because the condition on line 242 was never true
243 return
245 first_error = await scan_for_syntax_errors_and_token_level_diagnostics(
246 deb822_file,
247 lint_state,
248 )
249 header_stanza, source_stanza = _DWATCH_FILE_METADATA.stanza_types()
250 stanza_no = 0
252 async for stanza_range, stanza in lint_state.slow_iter(
253 with_range_in_continuous_parts(deb822_file.iter_parts())
254 ):
255 if not isinstance(stanza, Deb822ParagraphElement):
256 continue
257 stanza_position = stanza_range.start_pos
258 if stanza_position.line_position >= first_error: 258 ↛ 259line 258 didn't jump to line 259 because the condition on line 258 was never true
259 break
260 stanza_no += 1
261 is_source_stanza = stanza_no != 1
262 if is_source_stanza:
263 stanza_metadata = _DWATCH_FILE_METADATA.classify_stanza(
264 stanza,
265 stanza_no,
266 )
267 other_stanza_metadata = header_stanza
268 other_stanza_name = "Header"
269 elif "Version" in stanza: 269 ↛ 274line 269 didn't jump to line 274 because the condition on line 269 was always true
270 stanza_metadata = header_stanza
271 other_stanza_metadata = source_stanza
272 other_stanza_name = "Source"
273 else:
274 break
276 await stanza_metadata.stanza_diagnostics(
277 deb822_file,
278 stanza,
279 stanza_position,
280 lint_state,
281 confusable_with_stanza_name=other_stanza_name,
282 confusable_with_stanza_metadata=other_stanza_metadata,
283 )
286@lsp_will_save_wait_until(_DISPATCH_RULE)
287def _debian_watch_on_save_formatting(
288 ls: "DebputyLanguageServer",
289 params: WillSaveTextDocumentParams,
290) -> Sequence[TextEdit] | None:
291 doc = ls.workspace.get_text_document(params.text_document.uri)
292 lint_state = ls.lint_state(doc)
293 return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)
296@lsp_cli_reformat_document(_DISPATCH_RULE)
297def _reformat_debian_watch(
298 lint_state: LintState,
299) -> Sequence[TextEdit] | None:
300 return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)
303@lsp_format_document(_DISPATCH_RULE)
304def _debian_watch_format_doc(
305 ls: "DebputyLanguageServer",
306 params: DocumentFormattingParams,
307) -> Sequence[TextEdit] | None:
308 doc = ls.workspace.get_text_document(params.text_document.uri)
309 lint_state = ls.lint_state(doc)
310 return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)
313@lsp_semantic_tokens_full(_DISPATCH_RULE)
314async def _debian_watch_semantic_tokens_full(
315 ls: "DebputyLanguageServer",
316 request: SemanticTokensParams,
317) -> SemanticTokens | None:
318 return await deb822_semantic_tokens_full(
319 ls,
320 request,
321 _DWATCH_FILE_METADATA,
322 )