Skip to main content

rust_analyzer_mcp/
settings.rs

1//! The configuration rust-analyzer is asked to run with.
2//!
3//! rust-analyzer has no command line to speak of: everything it can be told is told to it over
4//! the LSP, as one JSON object. This is that object -- the settings this server always wants,
5//! with whatever the user asked for on top.
6
7use anyhow::{bail, Result};
8use serde_json::{json, Value};
9
10/// The cargo features to analyse and check with.
11#[derive(Clone, Debug, Default, PartialEq, Eq)]
12pub enum Features {
13    /// Whatever the manifest makes default.
14    #[default]
15    Default,
16    /// All of them, which is `--all-features`.
17    All,
18    /// The named ones, in the spellings cargo takes: a bare feature name, or `package/feature`
19    /// in a workspace.
20    Named(Vec<String>),
21}
22
23/// Everything the command line has to say about how rust-analyzer should run.
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
25pub struct Settings {
26    features: Features,
27    no_default_features: bool,
28    /// Settings named outright, as a path through the configuration and the value to put there.
29    overrides: Vec<(String, Value)>,
30}
31
32impl Settings {
33    /// Enables every feature.
34    pub fn enable_all_features(&mut self) -> Result<()> {
35        if self.features != Features::Default {
36            bail!("--all-features cannot be combined with --features");
37        }
38
39        self.features = Features::All;
40        Ok(())
41    }
42
43    /// Enables the features `list` names, comma- or space-separated.
44    pub fn enable_features(&mut self, list: &str) -> Result<()> {
45        let named: Vec<String> = list
46            .split([',', ' ', '\t'])
47            .filter(|feature| !feature.is_empty())
48            .map(str::to_string)
49            .collect();
50        if named.is_empty() {
51            bail!("--features needs at least one feature name");
52        }
53
54        match &mut self.features {
55            // rust-analyzer takes either every feature or a list, so the two cannot be asked for
56            // together: it would go with all of them and say nothing about the rest.
57            Features::All => bail!("--features cannot be combined with --all-features"),
58            Features::Named(features) => features.extend(named),
59            features @ Features::Default => *features = Features::Named(named),
60        }
61        Ok(())
62    }
63
64    /// Leaves the manifest's default features out.
65    pub fn disable_default_features(&mut self) -> Result<()> {
66        if self.features == Features::All {
67            bail!("--no-default-features cannot be combined with --all-features");
68        }
69
70        self.no_default_features = true;
71        Ok(())
72    }
73
74    /// Sets one rust-analyzer setting outright, from a `key.path=value` as it is spelled on the
75    /// command line.
76    ///
77    /// The value is read as JSON, and taken for a string when it is not any other JSON value --
78    /// so `check.command=clippy` means what it looks like it means.
79    pub fn set(&mut self, assignment: &str) -> Result<()> {
80        let Some((key, value)) = assignment.split_once('=') else {
81            bail!("--config needs a KEY=VALUE, such as check.command=clippy");
82        };
83        if key.is_empty() || key.split('.').any(str::is_empty) {
84            bail!("'{key}' is not a setting name");
85        }
86
87        let value = serde_json::from_str(value).unwrap_or_else(|_| json!(value));
88        self.overrides.push((key.to_string(), value));
89        Ok(())
90    }
91
92    /// The settings as rust-analyzer wants them.
93    pub fn to_json(&self) -> Value {
94        let mut settings = json!({
95            "cargo": {
96                "buildScripts": {
97                    "enable": true
98                }
99            },
100            "checkOnSave": true,
101            "diagnostics": {
102                "enable": true,
103                "experimental": {
104                    "enable": true
105                }
106            },
107            "procMacro": {
108                "enable": true
109            }
110        });
111
112        // `check.*` falls back to `cargo.*` for each of these, so setting them once covers both
113        // what rust-analyzer analyses and what cargo is asked to check.
114        match &self.features {
115            Features::Default => {}
116            Features::All => settings["cargo"]["features"] = json!("all"),
117            Features::Named(features) => settings["cargo"]["features"] = json!(features),
118        }
119        if self.no_default_features {
120            settings["cargo"]["noDefaultFeatures"] = json!(true);
121        }
122
123        for (key, value) in &self.overrides {
124            set_at(&mut settings, key, value.clone());
125        }
126
127        settings
128    }
129}
130
131/// Puts `value` at the dotted `key` in `settings`, making whatever objects it takes to get there.
132fn set_at(settings: &mut Value, key: &str, value: Value) {
133    let mut at = settings;
134    let mut names = key.split('.').peekable();
135
136    while let Some(name) = names.next() {
137        if names.peek().is_none() {
138            at[name] = value;
139            return;
140        }
141
142        // An override may name settings this server says nothing about, and may equally
143        // contradict one it does: either way what the user asked for wins.
144        if !at[name].is_object() {
145            at[name] = json!({});
146        }
147        at = &mut at[name];
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn the_defaults_leave_features_to_the_manifest() {
157        let settings = Settings::default().to_json();
158
159        assert_eq!(settings["cargo"]["features"], Value::Null);
160        assert_eq!(settings["cargo"]["noDefaultFeatures"], Value::Null);
161    }
162
163    #[test]
164    fn all_features_is_the_word_rust_analyzer_knows() {
165        let mut settings = Settings::default();
166        settings.enable_all_features().unwrap();
167
168        // Lower case, and the bare string rather than a list containing it: a list would name a
169        // feature actually called "all".
170        assert_eq!(settings.to_json()["cargo"]["features"], json!("all"));
171    }
172
173    #[test]
174    fn features_are_named_one_by_one() {
175        let mut settings = Settings::default();
176        settings.enable_features("serde,tokio").unwrap();
177        settings.enable_features("other/thing  extra").unwrap();
178
179        assert_eq!(
180            settings.to_json()["cargo"]["features"],
181            json!(["serde", "tokio", "other/thing", "extra"])
182        );
183    }
184
185    #[test]
186    fn features_that_name_nothing_are_refused() {
187        assert!(Settings::default().enable_features("").is_err());
188        assert!(Settings::default().enable_features(" , ").is_err());
189    }
190
191    #[test]
192    fn all_features_cannot_be_narrowed() {
193        // rust-analyzer would quietly go with all of them; better to say so.
194        let mut settings = Settings::default();
195        settings.enable_all_features().unwrap();
196
197        assert!(settings.enable_features("serde").is_err());
198        assert!(settings.disable_default_features().is_err());
199
200        let mut settings = Settings::default();
201        settings.enable_features("serde").unwrap();
202        assert!(settings.enable_all_features().is_err());
203    }
204
205    #[test]
206    fn default_features_can_be_left_out() {
207        let mut settings = Settings::default();
208        settings.disable_default_features().unwrap();
209        settings.enable_features("serde").unwrap();
210
211        let settings = settings.to_json();
212        assert_eq!(settings["cargo"]["noDefaultFeatures"], json!(true));
213        assert_eq!(settings["cargo"]["features"], json!(["serde"]));
214    }
215
216    #[test]
217    fn any_setting_can_be_named_outright() {
218        let mut settings = Settings::default();
219        settings.set("check.command=clippy").unwrap();
220        settings
221            .set("cargo.target=x86_64-unknown-linux-gnu")
222            .unwrap();
223        settings.set("check.extraArgs=[\"--tests\"]").unwrap();
224        settings.set("procMacro.enable=false").unwrap();
225
226        let settings = settings.to_json();
227        assert_eq!(settings["check"]["command"], json!("clippy"));
228        assert_eq!(
229            settings["cargo"]["target"],
230            json!("x86_64-unknown-linux-gnu")
231        );
232        assert_eq!(settings["check"]["extraArgs"], json!(["--tests"]));
233        // Including one this server has an opinion about.
234        assert_eq!(settings["procMacro"]["enable"], json!(false));
235    }
236
237    #[test]
238    fn an_override_replaces_what_stands_in_its_way() {
239        let mut settings = Settings::default();
240        settings.set("checkOnSave.enable=true").unwrap();
241
242        // `checkOnSave` is a bare boolean by default, and a path through it has to make it an
243        // object rather than sit next to it.
244        assert_eq!(settings.to_json()["checkOnSave"], json!({ "enable": true }));
245    }
246
247    #[test]
248    fn settings_that_name_nothing_are_refused() {
249        assert!(Settings::default().set("check.command").is_err());
250        assert!(Settings::default().set("=clippy").is_err());
251        assert!(Settings::default().set("check..command=clippy").is_err());
252    }
253}