Coverage for src/debputy/lsp/languages/lsp_debian_watch.py: 86%

142 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-12 15:06 +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 debputy.lsp.vendoring._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.util import _info 

69from debputy.yaml import MANIFEST_YAML 

70 

71try: 

72 from debputy.lsp.vendoring._deb822_repro.locatable import ( 

73 Position as TEPosition, 

74 Range as TERange, 

75 ) 

76 

77 from pygls.server import LanguageServer 

78 from pygls.workspace import TextDocument 

79except ImportError: 

80 pass 

81 

82 

83if TYPE_CHECKING: 

84 import lsprotocol.types as types 

85else: 

86 import debputy.lsprotocol.types as types 

87 

88 

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

90 

91_DISPATCH_RULE = LanguageDispatchRule.new_rule( 

92 "debian/watch", 

93 None, 

94 "debian/watch", 

95 [ 

96 # Presumably, emacs's name 

97 SecondaryLanguage("debian-watch"), 

98 # Presumably, vim's name 

99 SecondaryLanguage("debwatch"), 

100 ], 

101) 

102 

103_DWATCH_FILE_METADATA = DebianWatch5FileMetadata() 

104 

105lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION) 

106 

107 

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

109class VariableMetadata: 

110 name: str 

111 doc_uris: Sequence[str] 

112 synopsis: str 

113 description: str 

114 

115 def render_metadata_fields(self) -> str: 

116 doc_uris = self.doc_uris 

117 parts = [] 

118 if doc_uris: 118 ↛ 124line 118 didn't jump to line 124 because the condition on line 118 was always true

119 if len(doc_uris) == 1: 119 ↛ 122line 119 didn't jump to line 122 because the condition on line 119 was always true

120 parts.append(f"Documentation: {markdown_urlify(doc_uris[0])}") 

121 else: 

122 parts.append("Documentation:") 

123 parts.extend(f" - {markdown_urlify(uri)}" for uri in doc_uris) 

124 return "\n".join(parts) 

125 

126 @classmethod 

127 def from_ref_data(cls, x: GenericVariable) -> "Self": 

128 doc = x.get("documentation", {}) 

129 return cls( 

130 x["name"], 

131 doc.get("uris", []), 

132 doc.get("synopsis", ""), 

133 doc.get("long_description", ""), 

134 ) 

135 

136 

137def dwatch_variables_metadata_basename() -> str: 

138 return "debian_watch_variables_data.yaml" 

139 

140 

141def _as_variables_metadata( 

142 args: list[VariableMetadata], 

143) -> Mapping[str, VariableMetadata]: 

144 r = {s.name: s for s in args} 

145 assert len(r) == len(args) 

146 return r 

147 

148 

149@lru_cache 

150def dwatch_variables_metadata() -> Mapping[str, VariableMetadata]: 

151 p = importlib.resources.files(deb822_ref_data_dir.__name__).joinpath( 

152 dwatch_variables_metadata_basename() 

153 ) 

154 

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

156 raw = MANIFEST_YAML.load(fd) 

157 

158 attr_path = AttributePath.root_path(p) 

159 ref = GENERIC_VARIABLE_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

160 return _as_variables_metadata( 

161 [VariableMetadata.from_ref_data(x) for x in ref["variables"]] 

162 ) 

163 

164 

165def _custom_hover( 

166 ls: "DebputyLanguageServer", 

167 server_position: types.Position, 

168 _current_field: str | None, 

169 _word_at_position: str, 

170 _known_field: Deb822KnownField | None, 

171 in_value: bool, 

172 _doc: "TextDocument", 

173 lines: list[str], 

174) -> Hover | str | None: 

175 if not in_value: 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true

176 return None 

177 

178 line_no = server_position.line 

179 line = lines[line_no] 

180 variable_search_ref = server_position.character 

181 variable = "" 

182 try: 

183 # Unlike ${} substvars where the start and end uses distinct characters, we cannot 

184 # know for certain whether we are at the start or end of a variable when we land 

185 # directly on a separator. 

186 try: 

187 variable_start = line.rindex("@", 0, variable_search_ref) 

188 except ValueError: 

189 if line[variable_search_ref] != "@": 

190 raise 

191 variable_start = variable_search_ref 

192 

193 variable_end = line.index("@", variable_start + 1) 

194 if server_position.character <= variable_end: 194 ↛ 199line 194 didn't jump to line 199 because the condition on line 194 was always true

195 variable = line[variable_start : variable_end + 1] 

196 except (ValueError, IndexError): 

197 pass 

198 

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

200 substvar_md = dwatch_variables_metadata().get(variable) 

201 

202 if substvar_md is None: 202 ↛ 204line 202 didn't jump to line 204 because the condition on line 202 was never true

203 # In case of `@PACKAGE@-lin<CURSOR>ux-@ANY_VERSION@` 

204 return None 

205 doc = ls.translation(LSP_DATA_DOMAIN).pgettext( 

206 f"Variable:{substvar_md.name}", 

207 substvar_md.description, 

208 ) 

209 md_fields = "\n" + substvar_md.render_metadata_fields() 

210 return f"# Variable `{variable}`\n\n{doc}{md_fields}" 

211 

212 return None 

213 

214 

215@lsp_hover(_DISPATCH_RULE) 

216def _debian_watch_hover( 

217 ls: "DebputyLanguageServer", 

218 params: HoverParams, 

219) -> Hover | None: 

220 return deb822_hover(ls, params, _DWATCH_FILE_METADATA, custom_handler=_custom_hover) 

221 

222 

223@lsp_completer(_DISPATCH_RULE) 

224def _debian_watch_completions( 

225 ls: "DebputyLanguageServer", 

226 params: CompletionParams, 

227) -> CompletionList | Sequence[CompletionItem] | None: 

228 return deb822_completer(ls, params, _DWATCH_FILE_METADATA) 

229 

230 

231@lsp_folding_ranges(_DISPATCH_RULE) 

232def _debian_watch_folding_ranges( 

233 ls: "DebputyLanguageServer", 

234 params: FoldingRangeParams, 

235) -> Sequence[FoldingRange] | None: 

236 return deb822_folding_ranges(ls, params, _DWATCH_FILE_METADATA) 

237 

238 

239@lint_diagnostics(_DISPATCH_RULE) 

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

241 deb822_file = lint_state.parsed_deb822_file_content 

242 

243 if not _DWATCH_FILE_METADATA.file_metadata_applies_to_file(deb822_file): 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true

244 return 

245 

246 first_error = await scan_for_syntax_errors_and_token_level_diagnostics( 

247 deb822_file, 

248 lint_state, 

249 ) 

250 header_stanza, source_stanza = _DWATCH_FILE_METADATA.stanza_types() 

251 stanza_no = 0 

252 

253 async for stanza_range, stanza in lint_state.slow_iter( 

254 with_range_in_continuous_parts(deb822_file.iter_parts()) 

255 ): 

256 if not isinstance(stanza, Deb822ParagraphElement): 

257 continue 

258 stanza_position = stanza_range.start_pos 

259 if stanza_position.line_position >= first_error: 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true

260 break 

261 stanza_no += 1 

262 is_source_stanza = stanza_no != 1 

263 if is_source_stanza: 

264 stanza_metadata = _DWATCH_FILE_METADATA.classify_stanza( 

265 stanza, 

266 stanza_no, 

267 ) 

268 other_stanza_metadata = header_stanza 

269 other_stanza_name = "Header" 

270 elif "Version" in stanza: 270 ↛ 275line 270 didn't jump to line 275 because the condition on line 270 was always true

271 stanza_metadata = header_stanza 

272 other_stanza_metadata = source_stanza 

273 other_stanza_name = "Source" 

274 else: 

275 break 

276 

277 await stanza_metadata.stanza_diagnostics( 

278 deb822_file, 

279 stanza, 

280 stanza_position, 

281 lint_state, 

282 confusable_with_stanza_name=other_stanza_name, 

283 confusable_with_stanza_metadata=other_stanza_metadata, 

284 ) 

285 

286 

287@lsp_will_save_wait_until(_DISPATCH_RULE) 

288def _debian_watch_on_save_formatting( 

289 ls: "DebputyLanguageServer", 

290 params: WillSaveTextDocumentParams, 

291) -> Sequence[TextEdit] | None: 

292 doc = ls.workspace.get_text_document(params.text_document.uri) 

293 lint_state = ls.lint_state(doc) 

294 return deb822_format_file(lint_state, _DWATCH_FILE_METADATA) 

295 

296 

297@lsp_cli_reformat_document(_DISPATCH_RULE) 

298def _reformat_debian_watch( 

299 lint_state: LintState, 

300) -> Sequence[TextEdit] | None: 

301 return deb822_format_file(lint_state, _DWATCH_FILE_METADATA) 

302 

303 

304@lsp_format_document(_DISPATCH_RULE) 

305def _debian_watch_format_doc( 

306 ls: "DebputyLanguageServer", 

307 params: DocumentFormattingParams, 

308) -> Sequence[TextEdit] | None: 

309 doc = ls.workspace.get_text_document(params.text_document.uri) 

310 lint_state = ls.lint_state(doc) 

311 return deb822_format_file(lint_state, _DWATCH_FILE_METADATA) 

312 

313 

314@lsp_semantic_tokens_full(_DISPATCH_RULE) 

315async def _debian_watch_semantic_tokens_full( 

316 ls: "DebputyLanguageServer", 

317 request: SemanticTokensParams, 

318) -> SemanticTokens | None: 

319 return await deb822_semantic_tokens_full( 

320 ls, 

321 request, 

322 _DWATCH_FILE_METADATA, 

323 )