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
« 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)
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)
30from debputy.lsp.diagnostics import DiagnosticData
31from debputy.util import _warn
33try:
34 from debputy.lsp.vendoring._deb822_repro.locatable import (
35 Position as TEPosition,
36 Range as TERange,
37 )
39 from pygls.server import LanguageServer
40 from pygls.workspace import TextDocument
41except ImportError:
42 pass
45CodeActionName = Literal[
46 "correct-text",
47 "remove-line",
48 "remove-range",
49 "insert-text-on-line-after-diagnostic",
50]
53class CorrectTextCodeAction(TypedDict):
54 code_action: Literal["correct-text"]
55 correct_value: str
58class InsertTextOnLineAfterDiagnosticCodeAction(TypedDict):
59 code_action: Literal["insert-text-on-line-after-diagnostic"]
60 text_to_insert: str
63class RemoveLineCodeAction(TypedDict):
64 code_action: Literal["remove-line"]
67class RemoveRangeCodeAction(TypedDict):
68 code_action: Literal["remove-range"]
69 proposed_title: NotRequired[str]
72def propose_correct_text_quick_fix(correct_value: str) -> CorrectTextCodeAction:
73 return {
74 "code_action": "correct-text",
75 "correct_value": correct_value,
76 }
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 }
88def propose_remove_line_quick_fix() -> RemoveLineCodeAction:
89 return {
90 "code_action": "remove-line",
91 }
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
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]
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
126 return _wrapper
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 )
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 )
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
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
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 )
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 )
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