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

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) 

14 

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) 

21 

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 

71 

72try: 

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

74 Position as TEPosition, 

75 Range as TERange, 

76 ) 

77 

78 from pygls.server import LanguageServer 

79 from pygls.workspace import TextDocument 

80except ImportError: 

81 pass 

82 

83 

84if TYPE_CHECKING: 

85 import lsprotocol.types as types 

86else: 

87 import debputy.lsprotocol.types as types 

88 

89 

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

91 

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) 

103 

104_DWATCH_FILE_METADATA = DebianWatch5FileMetadata() 

105 

106lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION) 

107 

108 

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

110class VariableMetadata: 

111 name: str 

112 doc_uris: Sequence[str] 

113 synopsis: str 

114 description: str 

115 

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) 

126 

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 ) 

136 

137 

138def dwatch_variables_metadata_basename() -> str: 

139 return "debian_watch_variables_data.yaml" 

140 

141 

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 

148 

149 

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 ) 

155 

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

157 raw = MANIFEST_YAML.load(fd) 

158 

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 ) 

164 

165 

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 

178 

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 

193 

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 

199 

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

201 substvar_md = dwatch_variables_metadata().get(variable) 

202 

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

212 

213 return None 

214 

215 

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) 

222 

223 

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) 

230 

231 

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) 

238 

239 

240@lint_diagnostics(_DISPATCH_RULE) 

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

242 deb822_file = lint_state.parsed_deb822_file_content 

243 

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 

246 

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 

253 

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 

277 

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 ) 

286 

287 

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) 

296 

297 

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) 

303 

304 

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) 

313 

314 

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 )