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

115 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-03-24 16:38 +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 TYPE_CHECKING, 

15) 

16 

17from debputy.lsprotocol.types import ( 

18 CodeAction, 

19 Command, 

20 CodeActionParams, 

21 Diagnostic, 

22 TextEdit, 

23 WorkspaceEdit, 

24 TextDocumentEdit, 

25 OptionalVersionedTextDocumentIdentifier, 

26 Range, 

27 Position, 

28 CodeActionKind, 

29) 

30 

31from debputy.lsp.diagnostics import DiagnosticData 

32from debputy.util import _warn 

33 

34if TYPE_CHECKING: 

35 import lsprotocol.types as types 

36else: 

37 import debputy.lsprotocol.types as types 

38 

39 

40try: 

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

42 Position as TEPosition, 

43 Range as TERange, 

44 ) 

45 

46 from pygls.server import LanguageServer 

47 from pygls.workspace import TextDocument 

48 from debputy.lsp.debputy_ls import DebputyLanguageServer 

49except ImportError: 

50 pass 

51 

52 

53CodeActionName = Literal[ 

54 "correct-text", 

55 "remove-line", 

56 "remove-range", 

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

58] 

59 

60 

61class CorrectTextCodeAction(TypedDict): 

62 code_action: Literal["correct-text"] 

63 correct_value: str 

64 

65 

66class InsertTextOnLineAfterDiagnosticCodeAction(TypedDict): 

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

68 text_to_insert: str 

69 

70 

71class RemoveLineCodeAction(TypedDict): 

72 code_action: Literal["remove-line"] 

73 

74 

75class RemoveRangeCodeAction(TypedDict): 

76 code_action: Literal["remove-range"] 

77 proposed_title: NotRequired[str] 

78 

79 

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

81 return { 

82 "code_action": "correct-text", 

83 "correct_value": correct_value, 

84 } 

85 

86 

87def propose_insert_text_on_line_after_diagnostic_quick_fix( 

88 text_to_insert: str, 

89) -> InsertTextOnLineAfterDiagnosticCodeAction: 

90 return { 

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

92 "text_to_insert": text_to_insert, 

93 } 

94 

95 

96def propose_remove_line_quick_fix() -> RemoveLineCodeAction: 

97 return { 

98 "code_action": "remove-line", 

99 } 

100 

101 

102def propose_remove_range_quick_fix( 

103 *, 

104 proposed_title: Optional[str] = None, 

105) -> RemoveRangeCodeAction: 

106 r: RemoveRangeCodeAction = { 

107 "code_action": "remove-range", 

108 } 

109 if proposed_title: 

110 r["proposed_title"] = proposed_title 

111 return r 

112 

113 

114CODE_ACTION_HANDLERS: Dict[ 

115 CodeActionName, 

116 Callable[ 

117 [Mapping[str, str], CodeActionParams, Diagnostic], 

118 Iterable[Union[CodeAction, Command]], 

119 ], 

120] = {} 

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

122Handler = Callable[ 

123 [M, CodeActionParams, Diagnostic], 

124 Iterable[Union[CodeAction, Command]], 

125] 

126 

127 

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

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

130 assert action_name not in CODE_ACTION_HANDLERS 

131 CODE_ACTION_HANDLERS[action_name] = func 

132 return func 

133 

134 return _wrapper 

135 

136 

137@_code_handler_for("correct-text") 

138def _correct_value_code_action( 

139 code_action_data: CorrectTextCodeAction, 

140 code_action_params: CodeActionParams, 

141 diagnostic: Diagnostic, 

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

143 corrected_value = code_action_data["correct_value"] 

144 edit = TextEdit( 

145 diagnostic.range, 

146 corrected_value, 

147 ) 

148 yield CodeAction( 

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

150 kind=CodeActionKind.QuickFix, 

151 diagnostics=[diagnostic], 

152 edit=WorkspaceEdit( 

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

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 ), 

189 ) 

190 

191 

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

193 if isinstance(range_, TERange): 

194 start = range_.start_pos 

195 end = range_.end_pos 

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

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

198 ): 

199 return False 

200 else: 

201 start = range_.start 

202 end = range_.end 

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

204 return False 

205 return True 

206 

207 

208@_code_handler_for("remove-line") 

209def _remove_line_code_action( 

210 _code_action_data: RemoveLineCodeAction, 

211 code_action_params: CodeActionParams, 

212 diagnostic: Diagnostic, 

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

214 start = code_action_params.range.start 

215 if range_compatible_with_remove_line_fix(code_action_params.range): 

216 _warn( 

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

218 ) 

219 return 

220 

221 edit = TextEdit( 

222 Range( 

223 start=Position( 

224 line=start.line, 

225 character=0, 

226 ), 

227 end=Position( 

228 line=start.line + 1, 

229 character=0, 

230 ), 

231 ), 

232 "", 

233 ) 

234 yield CodeAction( 

235 title="Remove the line", 

236 kind=CodeActionKind.QuickFix, 

237 diagnostics=[diagnostic], 

238 edit=WorkspaceEdit( 

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

240 ), 

241 ) 

242 

243 

244@_code_handler_for("remove-range") 

245def _remove_range_code_action( 

246 code_action_data: RemoveRangeCodeAction, 

247 code_action_params: types.CodeActionParams, 

248 diagnostic: types.Diagnostic, 

249) -> Iterable[Union[types.CodeAction, types.Command]]: 

250 edit = types.TextEdit( 

251 diagnostic.range, 

252 "", 

253 ) 

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

255 yield CodeAction( 

256 title=title, 

257 kind=CodeActionKind.QuickFix, 

258 diagnostics=[diagnostic], 

259 edit=WorkspaceEdit( 

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

261 ), 

262 ) 

263 

264 

265def accepts_quickfixes( 

266 code_action_params: types.CodeActionParams, 

267) -> bool: 

268 only = code_action_params.context.only 

269 if not only: 

270 return True 

271 return types.CodeActionKind.QuickFix in only 

272 

273 

274def provide_standard_quickfixes_from_diagnostics_ls( 

275 ls: "DebputyLanguageServer", 

276 code_action_params: types.CodeActionParams, 

277) -> Optional[List[Union[types.Command, types.CodeAction]]]: 

278 if not accepts_quickfixes(code_action_params): 

279 return None 

280 matched_diagnostics = ls.diagnostics_in_range( 

281 code_action_params.text_document.uri, 

282 code_action_params.range, 

283 ) 

284 if not matched_diagnostics: 

285 return None 

286 return _provide_standard_quickfixes_from_diagnostics( 

287 code_action_params, matched_diagnostics 

288 ) 

289 

290 

291def provide_standard_quickfixes_from_diagnostics_lint( 

292 code_action_params: types.CodeActionParams, 

293) -> Optional[List[Union[types.Command, types.CodeAction]]]: 

294 return _provide_standard_quickfixes_from_diagnostics( 

295 code_action_params, code_action_params.context.diagnostics 

296 ) 

297 

298 

299def _provide_standard_quickfixes_from_diagnostics( 

300 code_action_params: types.CodeActionParams, 

301 matched_diagnostics: List[types.Diagnostic], 

302) -> Optional[List[Union[types.Command, types.CodeAction]]]: 

303 actions: List[Union[types.Command, types.CodeAction]] = [] 

304 for diagnostic in matched_diagnostics: 

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

306 continue 

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

308 quickfixes = data.get("quickfixes") 

309 if quickfixes is None: 

310 continue 

311 for action_suggestion in quickfixes: 

312 if ( 

313 action_suggestion 

314 and isinstance(action_suggestion, Mapping) 

315 and "code_action" in action_suggestion 

316 ): 

317 action_name: CodeActionName = action_suggestion["code_action"] 

318 handler = CODE_ACTION_HANDLERS.get(action_name) 

319 if handler is not None: 

320 actions.extend( 

321 handler( 

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

323 code_action_params, 

324 diagnostic, 

325 ) 

326 ) 

327 else: 

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

329 if not actions: 

330 return None 

331 return actions