bynkc_lsp/
code_actions.rs1use bynk_syntax::error::Applicability;
14use bynk_syntax::span::Span;
15use tower_lsp::lsp_types::*;
16
17pub fn quick_fixes(
21 text: &str,
22 diagnostics: &[bynk_ide::Diagnostic],
23 requested: Span,
24 uri: &Url,
25 version: Option<i32>,
26) -> Vec<CodeActionOrCommand> {
27 let mut out = Vec::new();
28 for d in diagnostics {
29 if !intersects(d.error.span, requested) {
30 continue;
31 }
32 for s in &d.error.suggestions {
33 if s.applicability != Applicability::MachineApplicable {
36 continue;
37 }
38 let edits: Vec<OneOf<TextEdit, AnnotatedTextEdit>> = s
39 .edits
40 .iter()
41 .map(|(span, replacement)| {
42 OneOf::Left(TextEdit {
43 range: crate::position::span_to_range(text, *span),
44 new_text: replacement.clone(),
45 })
46 })
47 .collect();
48 out.push(CodeActionOrCommand::CodeAction(CodeAction {
49 title: s.message.clone(),
50 kind: Some(CodeActionKind::QUICKFIX),
51 edit: Some(WorkspaceEdit {
52 changes: None,
53 document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit {
54 text_document: OptionalVersionedTextDocumentIdentifier {
55 uri: uri.clone(),
56 version,
57 },
58 edits,
59 }])),
60 change_annotations: None,
61 }),
62 ..Default::default()
63 }));
64 }
65 }
66 out
67}
68
69fn intersects(a: Span, b: Span) -> bool {
72 a.start <= b.end && b.start <= a.end
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use bynk_syntax::error::CompileError;
79
80 fn diag_with_suggestion() -> bynk_ide::Diagnostic {
81 bynk_ide::Diagnostic {
84 severity: bynk_syntax::Severity::Error,
85 error: CompileError::new(
86 "bynk.given.undeclared_capability",
87 Span::new(17, 21),
88 "capability `Used` is used but not listed",
89 )
90 .with_suggestion(
91 "add `Used` to the `given` clause",
92 vec![(Span::new(14, 14), ", Used".to_string())],
93 Applicability::MachineApplicable,
94 ),
95 }
96 }
97
98 #[test]
99 fn keyed_on_the_diagnostic_span_not_the_edit_span() {
100 let text = "-> T given Cap { Used.op() }";
101 let uri = Url::parse("file:///a.bynk").unwrap();
102 let on_diag = quick_fixes(
105 text,
106 &[diag_with_suggestion()],
107 Span::new(18, 18),
108 &uri,
109 Some(7),
110 );
111 assert_eq!(on_diag.len(), 1);
112 let on_edit = quick_fixes(
115 text,
116 &[diag_with_suggestion()],
117 Span::new(14, 14),
118 &uri,
119 Some(7),
120 );
121 assert!(on_edit.is_empty());
122 }
123
124 #[test]
125 fn action_carries_a_versioned_quickfix_edit() {
126 let text = "-> T given Cap { Used.op() }";
127 let uri = Url::parse("file:///a.bynk").unwrap();
128 let actions = quick_fixes(
129 text,
130 &[diag_with_suggestion()],
131 Span::new(17, 21),
132 &uri,
133 Some(7),
134 );
135 let CodeActionOrCommand::CodeAction(action) = &actions[0] else {
136 panic!("expected a CodeAction");
137 };
138 assert_eq!(action.title, "add `Used` to the `given` clause");
139 assert_eq!(action.kind, Some(CodeActionKind::QUICKFIX));
140 let Some(DocumentChanges::Edits(doc_edits)) =
141 &action.edit.as_ref().unwrap().document_changes
142 else {
143 panic!("expected versioned document edits");
144 };
145 assert_eq!(doc_edits[0].text_document.version, Some(7));
146 assert_eq!(doc_edits[0].text_document.uri, uri);
147 let OneOf::Left(edit) = &doc_edits[0].edits[0] else {
148 panic!("expected a plain TextEdit");
149 };
150 assert_eq!(edit.new_text, ", Used");
151 assert_eq!(edit.range.start, edit.range.end);
153 assert_eq!(edit.range.start.character, 14);
154 }
155
156 #[test]
157 fn placeholder_suggestions_are_not_offered() {
158 let text = "x";
159 let uri = Url::parse("file:///a.bynk").unwrap();
160 let d = bynk_ide::Diagnostic {
161 severity: bynk_syntax::Severity::Error,
162 error: CompileError::new("bynk.test", Span::new(0, 1), "msg").with_suggestion(
163 "fill in <T>",
164 vec![(Span::new(0, 1), "<T>".to_string())],
165 Applicability::HasPlaceholders,
166 ),
167 };
168 assert!(quick_fixes(text, &[d], Span::new(0, 1), &uri, None).is_empty());
169 }
170}