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