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