1use bynk_syntax::error::Applicability;
24use bynk_syntax::span::Span;
25use tower_lsp::lsp_types::*;
26
27pub fn kind() -> CodeActionKind {
29 CodeActionKind::new("source.fixAll.bynk")
30}
31
32pub fn edits(text: &str, diagnostics: &[bynk_ide::Diagnostic]) -> Vec<(Span, String)> {
37 let mut taken: Vec<(Span, String)> = Vec::new();
38 for d in diagnostics {
39 let mut applicable = d
40 .error
41 .suggestions
42 .iter()
43 .filter(|s| s.applicability == Applicability::MachineApplicable);
44 let (Some(s), None) = (applicable.next(), applicable.next()) else {
45 continue; };
47 if !s.edits.iter().all(|(span, _)| valid(text, *span)) {
48 continue;
49 }
50 let fresh: Vec<&(Span, String)> = s
51 .edits
52 .iter()
53 .filter(|e| !taken.iter().any(|t| t.0 == e.0 && t.1 == e.1))
54 .collect();
55 if fresh
56 .iter()
57 .any(|e| taken.iter().any(|t| overlaps(t.0, e.0)))
58 || fresh
59 .iter()
60 .enumerate()
61 .any(|(i, a)| fresh[i + 1..].iter().any(|b| overlaps(a.0, b.0)))
62 {
63 continue;
64 }
65 taken.extend(fresh.into_iter().cloned());
66 }
67 taken.sort_by_key(|(span, _)| (span.start, span.end));
68 taken
69}
70
71fn overlaps(a: Span, b: Span) -> bool {
75 if a.start == a.end && b.start == b.end {
76 return a.start == b.start;
77 }
78 a.start < b.end && b.start < a.end
79}
80
81fn valid(text: &str, span: Span) -> bool {
83 span.start <= span.end && text.get(span.start..span.end).is_some()
84}
85
86pub fn apply(text: &str, edits: &[(Span, String)]) -> String {
90 let mut out = String::with_capacity(text.len());
91 let mut last = 0;
92 for (span, new_text) in edits {
93 let Some(before) = text.get(last..span.start) else {
94 continue;
95 };
96 if !valid(text, *span) {
97 continue;
98 }
99 out.push_str(before);
100 out.push_str(new_text);
101 last = span.end;
102 }
103 out.push_str(&text[last..]);
104 out
105}
106
107pub fn action(
109 text: &str,
110 edits: &[(Span, String)],
111 uri: &Url,
112 version: Option<i32>,
113) -> CodeActionOrCommand {
114 CodeActionOrCommand::CodeAction(CodeAction {
115 title: "Fix all auto-fixable problems".to_string(),
116 kind: Some(kind()),
117 edit: Some(WorkspaceEdit {
118 changes: None,
119 document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit {
120 text_document: OptionalVersionedTextDocumentIdentifier {
121 uri: uri.clone(),
122 version,
123 },
124 edits: edits
125 .iter()
126 .map(|(span, new_text)| {
127 OneOf::Left(TextEdit {
128 range: crate::position::span_to_range(text, *span),
129 new_text: new_text.clone(),
130 })
131 })
132 .collect(),
133 }])),
134 change_annotations: None,
135 }),
136 ..Default::default()
137 })
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use bynk_syntax::error::CompileError;
144
145 type Fix<'a> = (&'a [((usize, usize), &'a str)], Applicability);
147
148 fn diag(span: (usize, usize), fixes: &[Fix]) -> bynk_ide::Diagnostic {
149 let mut error = CompileError::new("bynk.test", Span::new(span.0, span.1), "msg");
150 for (edits, applicability) in fixes {
151 error = error.with_suggestion(
152 "fix",
153 edits
154 .iter()
155 .map(|((s, e), t)| (Span::new(*s, *e), t.to_string()))
156 .collect(),
157 *applicability,
158 );
159 }
160 bynk_ide::Diagnostic {
161 severity: bynk_syntax::Severity::Error,
162 label_files: Vec::new(),
163 error,
164 }
165 }
166
167 const TEXT: &str = "0123456789012345678901234567890123456789";
169 const M: Applicability = Applicability::MachineApplicable;
170 const P: Applicability = Applicability::HasPlaceholders;
171
172 #[test]
173 fn takes_every_lone_machine_applicable_suggestion_in_position_order() {
174 let ds = [
175 diag((10, 12), &[(&[((10, 12), "b")], M)]),
176 diag((0, 2), &[(&[((0, 2), "a")], M)]),
177 ];
178 assert_eq!(
179 edits(TEXT, &ds),
180 vec![
181 (Span::new(0, 2), "a".to_string()),
182 (Span::new(10, 12), "b".to_string())
183 ]
184 );
185 }
186
187 #[test]
188 fn skips_placeholders_and_choices() {
189 let ds = [
190 diag((0, 2), &[(&[((0, 2), "<T>")], P)]),
191 diag((5, 6), &[(&[((5, 6), "x")], M), (&[((5, 6), "y")], M)]),
193 diag((8, 9), &[(&[((8, 9), "z")], M), (&[((8, 9), "<T>")], P)]),
195 ];
196 assert_eq!(edits(TEXT, &ds), vec![(Span::new(8, 9), "z".to_string())]);
197 }
198
199 #[test]
200 fn an_overlapping_suggestion_is_skipped_whole() {
201 let ds = [
202 diag((0, 4), &[(&[((0, 4), "a")], M)]),
203 diag((10, 12), &[(&[((10, 12), "b"), ((2, 3), "c")], M)]),
206 ];
207 assert_eq!(edits(TEXT, &ds), vec![(Span::new(0, 4), "a".to_string())]);
208 }
209
210 #[test]
211 fn an_identical_edit_is_taken_once() {
212 let ds = [
215 diag((20, 24), &[(&[((14, 14), ", Used")], M)]),
216 diag((30, 34), &[(&[((14, 14), ", Used")], M)]),
217 ];
218 assert_eq!(
219 edits(TEXT, &ds),
220 vec![(Span::new(14, 14), ", Used".to_string())]
221 );
222 }
223
224 #[test]
225 fn different_insertions_at_one_point_conflict() {
226 let ds = [
227 diag((20, 24), &[(&[((14, 14), ", A")], M)]),
228 diag((30, 34), &[(&[((14, 14), ", B")], M)]),
229 ];
230 assert_eq!(
231 edits(TEXT, &ds),
232 vec![(Span::new(14, 14), ", A".to_string())]
233 );
234 }
235
236 #[test]
237 fn touching_edits_do_not_conflict() {
238 let ds = [
239 diag((0, 2), &[(&[((0, 2), "a")], M)]),
240 diag((2, 4), &[(&[((2, 4), "b")], M)]),
241 diag((4, 4), &[(&[((4, 4), "c")], M)]),
242 ];
243 assert_eq!(edits(TEXT, &ds).len(), 3);
244 assert_eq!(apply("0123456", &edits(TEXT, &ds)), "abc456");
245 }
246
247 #[test]
248 fn an_insertion_inside_a_replacement_conflicts() {
249 let ds = [
250 diag((0, 4), &[(&[((0, 4), "a")], M)]),
251 diag((2, 2), &[(&[((2, 2), "b")], M)]),
252 ];
253 assert_eq!(edits(TEXT, &ds), vec![(Span::new(0, 4), "a".to_string())]);
254 }
255
256 #[test]
257 fn a_suggestion_with_a_bad_span_is_skipped() {
258 let ds = [
259 diag((0, 1), &[(&[((38, 99), "x")], M)]),
261 diag((0, 1), &[(&[((5, 3), "y")], M)]),
263 diag((0, 1), &[(&[((0, 1), "z")], M)]),
264 ];
265 assert_eq!(edits(TEXT, &ds), vec![(Span::new(0, 1), "z".to_string())]);
266 let ds = [diag((0, 1), &[(&[((2, 2), "w")], M)])];
268 assert!(edits("aé", &ds).is_empty());
269 }
270
271 #[test]
272 fn apply_skips_a_bad_span_instead_of_panicking() {
273 let bad = [
274 (Span::new(0, 1), "A".to_string()),
275 (Span::new(2, 99), "!".to_string()),
276 ];
277 assert_eq!(apply("abc", &bad), "Abc");
278 assert_eq!(apply("aé", &[(Span::new(2, 2), "w".to_string())]), "aé");
279 }
280
281 #[test]
282 fn nothing_to_fix_is_empty() {
283 assert!(edits(TEXT, &[]).is_empty());
284 assert!(edits(TEXT, &[diag((0, 1), &[])]).is_empty());
285 }
286}