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

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 

10 

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) 

17 

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 

66 

67try: 

68 from debian._deb822_repro.locatable import ( 

69 Position as TEPosition, 

70 Range as TERange, 

71 ) 

72 

73 from pygls.server import LanguageServer 

74 from pygls.workspace import TextDocument 

75except ImportError: 

76 pass 

77 

78 

79if TYPE_CHECKING: 

80 import lsprotocol.types as types 

81else: 

82 import debputy.lsprotocol.types as types 

83 

84 

85_CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]") 

86 

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) 

98 

99_DWATCH_FILE_METADATA = DebianWatch5FileMetadata() 

100 

101lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION) 

102 

103 

104@dataclasses.dataclass(slots=True, frozen=True) 

105class VariableMetadata: 

106 name: str 

107 doc_uris: Sequence[str] 

108 synopsis: str 

109 description: str 

110 

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) 

121 

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 ) 

131 

132 

133def dwatch_variables_metadata_basename() -> str: 

134 return "debian_watch_variables_data.yaml" 

135 

136 

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 

143 

144 

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 ) 

150 

151 with p.open("r", encoding="utf-8") as fd: 

152 raw = MANIFEST_YAML.load(fd) 

153 

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 ) 

159 

160 

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 

173 

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 

188 

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 

194 

195 if variable != "" and variable != "@@": 

196 substvar_md = dwatch_variables_metadata().get(variable) 

197 

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}" 

207 

208 return None 

209 

210 

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) 

217 

218 

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) 

225 

226 

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) 

233 

234 

235@lint_diagnostics(_DISPATCH_RULE) 

236async def _lint_debian_watch(lint_state: LintState) -> None: 

237 deb822_file = lint_state.parsed_deb822_file_content 

238 

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 

241 

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 

248 

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 

272 

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 ) 

281 

282 

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) 

291 

292 

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) 

298 

299 

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) 

308 

309 

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 )