Skip to main content

just_lsp/
quickfixer.rs

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      &parameters(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      &parameters(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      &parameters(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, &parameters(target, diagnostics))
233        .matching_diagnostics(target, "deprecated-function"),
234      vec![diagnostic(target, "deprecated-function")]
235    );
236  }
237}