Coverage for src/debputy/lsp/quickfixes.py: 45%

99 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +0000

1from typing import ( 

2 Literal, 

3 TypedDict, 

4 Callable, 

5 Iterable, 

6 Union, 

7 TypeVar, 

8 Mapping, 

9 Dict, 

10 Optional, 

11 List, 

12 cast, 

13 NotRequired, 

14) 

15 

16from debputy.lsprotocol.types import ( 

17 CodeAction, 

18 Command, 

19 CodeActionParams, 

20 Diagnostic, 

21 TextEdit, 

22 WorkspaceEdit, 

23 TextDocumentEdit, 

24 OptionalVersionedTextDocumentIdentifier, 

25 Range, 

26 Position, 

27 CodeActionKind, 

28) 

29 

30from debputy.lsp.diagnostics import DiagnosticData 

31from debputy.util import _warn 

32 

33try: 

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

35 Position as TEPosition, 

36 Range as TERange, 

37 ) 

38 

39 from pygls.server import LanguageServer 

40 from pygls.workspace import TextDocument 

41except ImportError: 

42 pass 

43 

44 

45CodeActionName = Literal[ 

46 "correct-text", 

47 "remove-line", 

48 "remove-range", 

49 "insert-text-on-line-after-diagnostic", 

50] 

51 

52 

53class CorrectTextCodeAction(TypedDict): 

54 code_action: Literal["correct-text"] 

55 correct_value: str 

56 

57 

58class InsertTextOnLineAfterDiagnosticCodeAction(TypedDict): 

59 code_action: Literal["insert-text-on-line-after-diagnostic"] 

60 text_to_insert: str 

61 

62 

63class RemoveLineCodeAction(TypedDict): 

64 code_action: Literal["remove-line"] 

65 

66 

67class RemoveRangeCodeAction(TypedDict): 

68 code_action: Literal["remove-range"] 

69 proposed_title: NotRequired[str] 

70 

71 

72def propose_correct_text_quick_fix(correct_value: str) -> CorrectTextCodeAction: 

73 return { 

74 "code_action": "correct-text", 

75 "correct_value": correct_value, 

76 } 

77 

78 

79def propose_insert_text_on_line_after_diagnostic_quick_fix( 

80 text_to_insert: str, 

81) -> InsertTextOnLineAfterDiagnosticCodeAction: 

82 return { 

83 "code_action": "insert-text-on-line-after-diagnostic", 

84 "text_to_insert": text_to_insert, 

85 } 

86 

87 

88def propose_remove_line_quick_fix() -> RemoveLineCodeAction: 

89 return { 

90 "code_action": "remove-line", 

91 } 

92 

93 

94def propose_remove_range_quick_fix( 

95 *, 

96 proposed_title: Optional[str] = None, 

97) -> RemoveRangeCodeAction: 

98 r: RemoveRangeCodeAction = { 

99 "code_action": "remove-range", 

100 } 

101 if proposed_title: 

102 r["proposed_title"] = proposed_title 

103 return r 

104 

105 

106CODE_ACTION_HANDLERS: Dict[ 

107 CodeActionName, 

108 Callable[ 

109 [Mapping[str, str], CodeActionParams, Diagnostic], 

110 Iterable[Union[CodeAction, Command]], 

111 ], 

112] = {} 

113M = TypeVar("M", bound=Mapping[str, str]) 

114Handler = Callable[ 

115 [M, CodeActionParams, Diagnostic], 

116 Iterable[Union[CodeAction, Command]], 

117] 

118 

119 

120def _code_handler_for(action_name: CodeActionName) -> Callable[[Handler], Handler]: 

121 def _wrapper(func: Handler) -> Handler: 

122 assert action_name not in CODE_ACTION_HANDLERS 

123 CODE_ACTION_HANDLERS[action_name] = func 

124 return func 

125 

126 return _wrapper 

127 

128 

129@_code_handler_for("correct-text") 

130def _correct_value_code_action( 

131 code_action_data: CorrectTextCodeAction, 

132 code_action_params: CodeActionParams, 

133 diagnostic: Diagnostic, 

134) -> Iterable[Union[CodeAction, Command]]: 

135 corrected_value = code_action_data["correct_value"] 

136 edit = TextEdit( 

137 diagnostic.range, 

138 corrected_value, 

139 ) 

140 yield CodeAction( 

141 title=f'Replace with "{corrected_value}"', 

142 kind=CodeActionKind.QuickFix, 

143 diagnostics=[diagnostic], 

144 edit=WorkspaceEdit( 

145 changes={code_action_params.text_document.uri: [edit]}, 

146 document_changes=[ 

147 TextDocumentEdit( 

148 text_document=OptionalVersionedTextDocumentIdentifier( 

149 uri=code_action_params.text_document.uri, 

150 ), 

151 edits=[edit], 

152 ) 

153 ], 

154 ), 

155 ) 

156 

157 

158@_code_handler_for("insert-text-on-line-after-diagnostic") 

159def _insert_text_on_line_after_diagnostic_code_action( 

160 code_action_data: InsertTextOnLineAfterDiagnosticCodeAction, 

161 code_action_params: CodeActionParams, 

162 diagnostic: Diagnostic, 

163) -> Iterable[Union[CodeAction, Command]]: 

164 corrected_value = code_action_data["text_to_insert"] 

165 line_no = diagnostic.range.end.line 

166 if diagnostic.range.end.character > 0: 

167 line_no += 1 

168 insert_range = Range( 

169 Position( 

170 line_no, 

171 0, 

172 ), 

173 Position( 

174 line_no, 

175 0, 

176 ), 

177 ) 

178 edit = TextEdit( 

179 insert_range, 

180 corrected_value, 

181 ) 

182 yield CodeAction( 

183 title=f'Insert "{corrected_value}"', 

184 kind=CodeActionKind.QuickFix, 

185 diagnostics=[diagnostic], 

186 edit=WorkspaceEdit( 

187 changes={code_action_params.text_document.uri: [edit]}, 

188 document_changes=[ 

189 TextDocumentEdit( 

190 text_document=OptionalVersionedTextDocumentIdentifier( 

191 uri=code_action_params.text_document.uri, 

192 ), 

193 edits=[edit], 

194 ) 

195 ], 

196 ), 

197 ) 

198 

199 

200def range_compatible_with_remove_line_fix(range_: Union[Range, TERange]) -> bool: 

201 if isinstance(range_, TERange): 

202 start = range_.start_pos 

203 end = range_.end_pos 

204 if start.line_position != end.line_position and ( 

205 start.line_position + 1 != end.line_position or end.cursor_position > 0 

206 ): 

207 return False 

208 else: 

209 start = range_.start 

210 end = range_.end 

211 if start.line != end.line and (start.line + 1 != end.line or end.character > 0): 

212 return False 

213 return True 

214 

215 

216@_code_handler_for("remove-line") 

217def _remove_line_code_action( 

218 _code_action_data: RemoveLineCodeAction, 

219 code_action_params: CodeActionParams, 

220 diagnostic: Diagnostic, 

221) -> Iterable[Union[CodeAction, Command]]: 

222 start = code_action_params.range.start 

223 if range_compatible_with_remove_line_fix(code_action_params.range): 

224 _warn( 

225 "Bug: the quick was used for a diagnostic that spanned multiple lines and would corrupt the file." 

226 ) 

227 return 

228 

229 edit = TextEdit( 

230 Range( 

231 start=Position( 

232 line=start.line, 

233 character=0, 

234 ), 

235 end=Position( 

236 line=start.line + 1, 

237 character=0, 

238 ), 

239 ), 

240 "", 

241 ) 

242 yield CodeAction( 

243 title="Remove the line", 

244 kind=CodeActionKind.QuickFix, 

245 diagnostics=[diagnostic], 

246 edit=WorkspaceEdit( 

247 changes={code_action_params.text_document.uri: [edit]}, 

248 document_changes=[ 

249 TextDocumentEdit( 

250 text_document=OptionalVersionedTextDocumentIdentifier( 

251 uri=code_action_params.text_document.uri, 

252 ), 

253 edits=[edit], 

254 ) 

255 ], 

256 ), 

257 ) 

258 

259 

260@_code_handler_for("remove-range") 

261def _remove_range_code_action( 

262 code_action_data: RemoveRangeCodeAction, 

263 code_action_params: CodeActionParams, 

264 diagnostic: Diagnostic, 

265) -> Iterable[Union[CodeAction, Command]]: 

266 edit = TextEdit( 

267 diagnostic.range, 

268 "", 

269 ) 

270 title = code_action_data.get("proposed_title", "Delete") 

271 yield CodeAction( 

272 title=title, 

273 kind=CodeActionKind.QuickFix, 

274 diagnostics=[diagnostic], 

275 edit=WorkspaceEdit( 

276 changes={code_action_params.text_document.uri: [edit]}, 

277 document_changes=[ 

278 TextDocumentEdit( 

279 text_document=OptionalVersionedTextDocumentIdentifier( 

280 uri=code_action_params.text_document.uri, 

281 ), 

282 edits=[edit], 

283 ) 

284 ], 

285 ), 

286 ) 

287 

288 

289def provide_standard_quickfixes_from_diagnostics( 

290 code_action_params: CodeActionParams, 

291) -> Optional[List[Union[Command, CodeAction]]]: 

292 actions: List[Union[Command, CodeAction]] = [] 

293 for diagnostic in code_action_params.context.diagnostics: 

294 if not isinstance(diagnostic.data, dict): 

295 continue 

296 data: DiagnosticData = cast("DiagnosticData", diagnostic.data) 

297 quickfixes = data.get("quickfixes") 

298 if quickfixes is None: 

299 continue 

300 for action_suggestion in quickfixes: 

301 if ( 

302 action_suggestion 

303 and isinstance(action_suggestion, Mapping) 

304 and "code_action" in action_suggestion 

305 ): 

306 action_name: CodeActionName = action_suggestion["code_action"] 

307 handler = CODE_ACTION_HANDLERS.get(action_name) 

308 if handler is not None: 

309 actions.extend( 

310 handler( 

311 cast("Mapping[str, str]", action_suggestion), 

312 code_action_params, 

313 diagnostic, 

314 ) 

315 ) 

316 else: 

317 _warn(f"No codeAction handler for {action_name} !?") 

318 if not actions: 

319 return None 

320 return actions