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

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 

13 

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) 

20 

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 

69 

70try: 

71 from debian._deb822_repro.locatable import ( 

72 Position as TEPosition, 

73 Range as TERange, 

74 ) 

75 

76 from pygls.server import LanguageServer 

77 from pygls.workspace import TextDocument 

78except ImportError: 

79 pass 

80 

81 

82if TYPE_CHECKING: 

83 import lsprotocol.types as types 

84else: 

85 import debputy.lsprotocol.types as types 

86 

87 

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

89 

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) 

101 

102_DWATCH_FILE_METADATA = DebianWatch5FileMetadata() 

103 

104lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION) 

105 

106 

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

108class VariableMetadata: 

109 name: str 

110 doc_uris: Sequence[str] 

111 synopsis: str 

112 description: str 

113 

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) 

124 

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 ) 

134 

135 

136def dwatch_variables_metadata_basename() -> str: 

137 return "debian_watch_variables_data.yaml" 

138 

139 

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 

146 

147 

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 ) 

153 

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

155 raw = MANIFEST_YAML.load(fd) 

156 

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 ) 

162 

163 

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 

176 

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 

191 

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 

197 

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

199 substvar_md = dwatch_variables_metadata().get(variable) 

200 

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

210 

211 return None 

212 

213 

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) 

220 

221 

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) 

228 

229 

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) 

236 

237 

238@lint_diagnostics(_DISPATCH_RULE) 

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

240 deb822_file = lint_state.parsed_deb822_file_content 

241 

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 

244 

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 

251 

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 

275 

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 ) 

284 

285 

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) 

294 

295 

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) 

301 

302 

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) 

311 

312 

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 )