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
« 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)
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)
31from debputy.lsp.diagnostics import DiagnosticData
32from debputy.util import _warn
34if TYPE_CHECKING:
35 import lsprotocol.types as types
36else:
37 import debputy.lsprotocol.types as types
40try:
41 from debputy.lsp.vendoring._deb822_repro.locatable import (
42 Position as TEPosition,
43 Range as TERange,
44 )
46 from pygls.server import LanguageServer
47 from pygls.workspace import TextDocument
48 from debputy.lsp.debputy_ls import DebputyLanguageServer
49except ImportError:
50 pass
53CodeActionName = Literal[
54 "correct-text",
55 "remove-line",
56 "remove-range",
57 "insert-text-on-line-after-diagnostic",
58]
61class CorrectTextCodeAction(TypedDict):
62 code_action: Literal["correct-text"]
63 correct_value: str
66class InsertTextOnLineAfterDiagnosticCodeAction(TypedDict):
67 code_action: Literal["insert-text-on-line-after-diagnostic"]
68 text_to_insert: str
71class RemoveLineCodeAction(TypedDict):
72 code_action: Literal["remove-line"]
75class RemoveRangeCodeAction(TypedDict):
76 code_action: Literal["remove-range"]
77 proposed_title: NotRequired[str]
80def propose_correct_text_quick_fix(correct_value: str) -> CorrectTextCodeAction:
81 return {
82 "code_action": "correct-text",
83 "correct_value": correct_value,
84 }
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 }
96def propose_remove_line_quick_fix() -> RemoveLineCodeAction:
97 return {
98 "code_action": "remove-line",
99 }
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
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]
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
134 return _wrapper
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 )
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 )
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
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
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 )
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 )
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
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 )
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 )
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