1use bynk_syntax::error::Applicability;
27use bynk_syntax::span::Span;
28use tower_lsp::lsp_types::*;
29
30const CATEGORIES: [&str; 2] = [
33 "bynk.types.non_exhaustive_match",
34 "bynk.types.extensible_match_needs_wildcard",
35];
36const MESSAGE_PREFIX: &str = "add a match arm for ";
37const DATA_TAG: &str = "bynk.fillMatchArms";
38
39pub fn fillable(diagnostics: &[bynk_ide::Diagnostic], requested: Span) -> Vec<Span> {
43 let mut spans: Vec<Span> = diagnostics
44 .iter()
45 .filter(|d| CATEGORIES.contains(&d.error.category))
46 .map(|d| d.error.span)
47 .filter(|s| s.start <= requested.end && requested.start <= s.end)
48 .collect();
49 spans.sort_by_key(|s| (s.start, s.end));
50 spans.dedup_by(|a, b| same_span(*a, *b));
51 spans.retain(|s| arms(diagnostics, *s).is_some());
52 spans
53}
54
55fn same_span(a: Span, b: Span) -> bool {
58 (a.start, a.end) == (b.start, b.end)
59}
60
61fn arms(diagnostics: &[bynk_ide::Diagnostic], match_span: Span) -> Option<(usize, Vec<String>)> {
65 let mut anchor = None;
66 let mut out = Vec::new();
67 for d in diagnostics
68 .iter()
69 .filter(|d| CATEGORIES.contains(&d.error.category) && same_span(d.error.span, match_span))
70 {
71 let s = d.error.suggestions.iter().find(|s| {
72 s.applicability == Applicability::HasPlaceholders
73 && s.message.starts_with(MESSAGE_PREFIX)
74 })?;
75 let [(span, text)] = s.edits.as_slice() else {
76 return None;
77 };
78 if *anchor.get_or_insert(span.start) != span.start || span.start != span.end {
79 return None;
80 }
81 let arm = text.trim_start_matches('\n').to_string();
82 if !out.contains(&arm) {
83 out.push(arm);
84 }
85 }
86 Some((anchor?, out))
87}
88
89pub fn edits(
94 text: &str,
95 diagnostics: &[bynk_ide::Diagnostic],
96 match_span: Span,
97) -> Option<Vec<(Span, String)>> {
98 let (anchor, arms) = arms(diagnostics, match_span)?;
99 let line_start = text[..anchor].rfind('\n').map_or(0, |i| i + 1);
100 let line_end = text[anchor..].find('\n').map_or(text.len(), |i| anchor + i);
101 let indent: String = text[line_start..]
102 .chars()
103 .take_while(|c| *c == ' ' || *c == '\t')
104 .collect();
105 let rest = text[anchor..line_end].trim_start();
106 let (has_comma, after) = match rest.strip_prefix(',') {
107 Some(after) => (true, after.trim_start()),
108 None => (false, rest),
109 };
110 if !after.is_empty() && !after.starts_with("//") {
111 return None;
112 }
113 let mut out = Vec::new();
114 if !has_comma {
115 out.push((Span::new(anchor, anchor), ",".to_string()));
116 }
117 let inserted: String = arms.iter().map(|a| format!("\n{indent}{a},")).collect();
118 out.push((Span::new(line_end, line_end), inserted));
119
120 let filled = crate::fix_all::apply(text, &out);
121 let opts = bynk_fmt::FormatOptions::default();
122 let canonical = |s: &str| bynk_fmt::format_source(s, &opts).is_ok_and(|f| f == s);
123 if canonical(text) && !canonical(&filled) {
124 return None;
125 }
126 Some(out)
127}
128
129pub fn listed(
133 diagnostics: &[bynk_ide::Diagnostic],
134 match_span: Span,
135 uri: &Url,
136 version: Option<i32>,
137) -> CodeActionOrCommand {
138 let count = arms(diagnostics, match_span).map_or(0, |(_, arms)| arms.len());
140 CodeActionOrCommand::CodeAction(CodeAction {
141 title: if count == 1 {
142 "Fill the missing match arm".to_string()
143 } else {
144 format!("Fill {count} missing match arms")
145 },
146 kind: Some(CodeActionKind::QUICKFIX),
147 data: Some(serde_json::json!({
148 "kind": DATA_TAG,
149 "uri": uri.as_str(),
150 "version": version,
151 "start": match_span.start,
152 "end": match_span.end,
153 })),
154 ..Default::default()
155 })
156}
157
158pub struct Target {
161 pub uri: Url,
162 pub version: Option<i32>,
163 pub match_span: Span,
164}
165
166impl Target {
167 pub fn of(action: &CodeAction) -> Option<Self> {
168 let data = action.data.as_ref()?;
169 if data.get("kind")?.as_str()? != DATA_TAG {
170 return None;
171 }
172 Some(Target {
173 uri: Url::parse(data.get("uri")?.as_str()?).ok()?,
174 version: data
175 .get("version")
176 .and_then(serde_json::Value::as_i64)
177 .and_then(|v| i32::try_from(v).ok()),
178 match_span: Span::new(
179 usize::try_from(data.get("start")?.as_u64()?).ok()?,
180 usize::try_from(data.get("end")?.as_u64()?).ok()?,
181 ),
182 })
183 }
184}
185
186pub fn resolve(
188 mut action: CodeAction,
189 text: &str,
190 edits: &[(Span, String)],
191 uri: &Url,
192 version: Option<i32>,
193) -> CodeAction {
194 action.edit = Some(WorkspaceEdit {
195 changes: None,
196 document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit {
197 text_document: OptionalVersionedTextDocumentIdentifier {
198 uri: uri.clone(),
199 version,
200 },
201 edits: edits
202 .iter()
203 .map(|(span, new_text)| {
204 OneOf::Left(TextEdit {
205 range: crate::position::span_to_range(text, *span),
206 new_text: new_text.clone(),
207 })
208 })
209 .collect(),
210 }])),
211 change_annotations: None,
212 });
213 action
214}