1use super::*;
2
3pub struct Quickfixer<'a> {
4 document: &'a Document,
5 parameters: &'a lsp::CodeActionParams,
6}
7
8impl<'a> Quickfixer<'a> {
9 #[must_use]
10 pub fn collect(&self) -> Vec<lsp::CodeActionOrCommand> {
11 self
12 .deprecated_replacements()
13 .into_iter()
14 .filter(|(name, _, _)| name.range.overlaps(self.parameters.range))
15 .map(|(name, code, replacement)| {
16 self.replacement_action(&name, code, replacement)
17 })
18 .collect()
19 }
20
21 fn deprecated_replacements(
22 &self,
23 ) -> Vec<(TextNode, &'static str, &'static str)> {
24 let functions =
25 self
26 .document
27 .function_calls()
28 .into_iter()
29 .filter_map(|call| {
30 let replacement =
31 BUILTINS.iter().find_map(|builtin| match builtin {
32 Builtin::Function {
33 name,
34 deprecated: Some(replacement),
35 ..
36 } if *name == call.name.value => Some(*replacement),
37 _ => None,
38 })?;
39
40 Some((call.name, "deprecated-function", replacement))
41 });
42
43 let settings = self.document.settings().into_iter().filter_map(|setting| {
44 let replacement = BUILTINS.iter().find_map(|builtin| match builtin {
45 Builtin::Setting {
46 name,
47 deprecated: Some(replacement),
48 ..
49 } if *name == setting.name.value => Some(*replacement),
50 _ => None,
51 })?;
52
53 Some((setting.name, "deprecated-setting", replacement))
54 });
55
56 functions.chain(settings).collect()
57 }
58
59 fn matching_diagnostics(
60 &self,
61 range: lsp::Range,
62 code: &str,
63 ) -> Vec<lsp::Diagnostic> {
64 self
65 .parameters
66 .context
67 .diagnostics
68 .iter()
69 .filter(|diagnostic| {
70 diagnostic.range == range
71 && matches!(
72 &diagnostic.code,
73 Some(lsp::NumberOrString::String(c)) if c == code
74 )
75 })
76 .cloned()
77 .collect()
78 }
79
80 #[must_use]
81 pub fn new(
82 document: &'a Document,
83 parameters: &'a lsp::CodeActionParams,
84 ) -> Self {
85 Self {
86 document,
87 parameters,
88 }
89 }
90
91 fn replacement_action(
92 &self,
93 name: &TextNode,
94 code: &str,
95 replacement: &str,
96 ) -> lsp::CodeActionOrCommand {
97 let diagnostics = self.matching_diagnostics(name.range, code);
98
99 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
100 title: format!("Replace `{}` with `{}`", name.value, replacement),
101 kind: Some(lsp::CodeActionKind::QUICKFIX),
102 diagnostics: (!diagnostics.is_empty()).then_some(diagnostics),
103 edit: Some(lsp::WorkspaceEdit {
104 changes: Some(HashMap::from([(
105 self.parameters.text_document.uri.clone(),
106 vec![lsp::TextEdit {
107 range: name.range,
108 new_text: replacement.to_string(),
109 }],
110 )])),
111 ..Default::default()
112 }),
113 ..Default::default()
114 })
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use {super::*, pretty_assertions::assert_eq};
121
122 fn diagnostic(range: lsp::Range, code: &str) -> lsp::Diagnostic {
123 lsp::Diagnostic {
124 range,
125 code: Some(lsp::NumberOrString::String(code.to_string())),
126 ..Default::default()
127 }
128 }
129
130 fn parameters(
131 range: lsp::Range,
132 diagnostics: Vec<lsp::Diagnostic>,
133 ) -> lsp::CodeActionParams {
134 lsp::CodeActionParams {
135 text_document: lsp::TextDocumentIdentifier {
136 uri: lsp::Url::parse("file:///test.just").unwrap(),
137 },
138 range,
139 context: lsp::CodeActionContext {
140 diagnostics,
141 ..Default::default()
142 },
143 work_done_progress_params: lsp::WorkDoneProgressParams::default(),
144 partial_result_params: lsp::PartialResultParams::default(),
145 }
146 }
147
148 #[test]
149 fn collect_filters_multiple_calls_by_range() {
150 let document = Document::from(
151 "foo := env_var(\"A\")\nbar := env_var_or_default(\"B\", \"C\")\n",
152 );
153
154 let actions = Quickfixer::new(
155 &document,
156 ¶meters(lsp::Range::at(0, 10, 0, 10), vec![]),
157 )
158 .collect();
159
160 assert_eq!(actions.len(), 1);
161
162 let lsp::CodeActionOrCommand::CodeAction(action) = &actions[0] else {
163 unreachable!("expected CodeAction");
164 };
165
166 assert_eq!(action.title, "Replace `env_var` with `env`");
167 }
168
169 #[test]
170 fn collect_replaces_deprecated_setting() {
171 let document = Document::from("set windows-powershell := true\n");
172
173 let actions = Quickfixer::new(
174 &document,
175 ¶meters(lsp::Range::at(0, 4, 0, 4), vec![]),
176 )
177 .collect();
178
179 assert_eq!(actions.len(), 1);
180
181 let lsp::CodeActionOrCommand::CodeAction(action) = &actions[0] else {
182 unreachable!("expected CodeAction");
183 };
184
185 assert_eq!(
186 action.title,
187 "Replace `windows-powershell` with `windows-shell`"
188 );
189
190 assert_eq!(
191 action.edit,
192 Some(lsp::WorkspaceEdit {
193 changes: Some(HashMap::from([(
194 lsp::Url::parse("file:///test.just").unwrap(),
195 vec![lsp::TextEdit {
196 range: lsp::Range::at(0, 4, 0, 22),
197 new_text: "windows-shell".to_string(),
198 }],
199 )])),
200 ..Default::default()
201 }),
202 );
203 }
204
205 #[test]
206 fn collect_ignores_setting_outside_range() {
207 let document =
208 Document::from("set windows-powershell := true\nset export := true\n");
209
210 let actions = Quickfixer::new(
211 &document,
212 ¶meters(lsp::Range::at(1, 4, 1, 4), vec![]),
213 )
214 .collect();
215
216 assert_eq!(actions, vec![]);
217 }
218
219 #[test]
220 fn matching_diagnostics_filters_by_code_and_range() {
221 let document = Document::from("foo := env_var(\"A\")\n");
222
223 let target = lsp::Range::at(0, 7, 0, 14);
224
225 let diagnostics = vec![
226 diagnostic(target, "deprecated-function"),
227 diagnostic(target, "other-rule"),
228 diagnostic(lsp::Range::at(1, 0, 1, 5), "deprecated-function"),
229 ];
230
231 assert_eq!(
232 Quickfixer::new(&document, ¶meters(target, diagnostics))
233 .matching_diagnostics(target, "deprecated-function"),
234 vec![diagnostic(target, "deprecated-function")]
235 );
236 }
237}