vscode_import.rs

  1use anyhow::{Context as _, Result, anyhow};
  2use fs::Fs;
  3use paths::{cursor_settings_file_paths, vscode_settings_file_paths};
  4use serde_json::{Map, Value};
  5use std::{path::Path, sync::Arc};
  6
  7use crate::FontFamilyName;
  8
  9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
 10pub enum VsCodeSettingsSource {
 11    VsCode,
 12    Cursor,
 13}
 14
 15impl std::fmt::Display for VsCodeSettingsSource {
 16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 17        match self {
 18            VsCodeSettingsSource::VsCode => write!(f, "VS Code"),
 19            VsCodeSettingsSource::Cursor => write!(f, "Cursor"),
 20        }
 21    }
 22}
 23
 24pub struct VsCodeSettings {
 25    pub source: VsCodeSettingsSource,
 26    pub path: Arc<Path>,
 27    content: Map<String, Value>,
 28}
 29
 30impl VsCodeSettings {
 31    #[cfg(any(test, feature = "test-support"))]
 32    pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
 33        Ok(Self {
 34            source,
 35            path: Path::new("/example-path/Code/User/settings.json").into(),
 36            content: serde_json_lenient::from_str(content)?,
 37        })
 38    }
 39
 40    pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> {
 41        let candidate_paths = match source {
 42            VsCodeSettingsSource::VsCode => vscode_settings_file_paths(),
 43            VsCodeSettingsSource::Cursor => cursor_settings_file_paths(),
 44        };
 45        let mut path = None;
 46        for candidate_path in candidate_paths.iter() {
 47            if fs.is_file(candidate_path).await {
 48                path = Some(candidate_path.clone());
 49            }
 50        }
 51        let Some(path) = path else {
 52            return Err(anyhow!(
 53                "No settings file found, expected to find it in one of the following paths:\n{}",
 54                candidate_paths
 55                    .into_iter()
 56                    .map(|path| path.to_string_lossy().into_owned())
 57                    .collect::<Vec<_>>()
 58                    .join("\n")
 59            ));
 60        };
 61        let content = fs.load(&path).await.with_context(|| {
 62            format!(
 63                "Error loading {} settings file from {}",
 64                source,
 65                path.display()
 66            )
 67        })?;
 68        let content = serde_json_lenient::from_str(&content).with_context(|| {
 69            format!(
 70                "Error parsing {} settings file from {}",
 71                source,
 72                path.display()
 73            )
 74        })?;
 75        Ok(Self {
 76            source,
 77            path: path.into(),
 78            content,
 79        })
 80    }
 81
 82    pub fn read_value(&self, setting: &str) -> Option<&Value> {
 83        self.content.get(setting)
 84    }
 85
 86    pub fn read_string(&self, setting: &str) -> Option<&str> {
 87        self.read_value(setting).and_then(|v| v.as_str())
 88    }
 89
 90    pub fn read_bool(&self, setting: &str) -> Option<bool> {
 91        self.read_value(setting).and_then(|v| v.as_bool())
 92    }
 93
 94    pub fn string_setting(&self, key: &str, setting: &mut Option<String>) {
 95        if let Some(s) = self.content.get(key).and_then(Value::as_str) {
 96            *setting = Some(s.to_owned())
 97        }
 98    }
 99
100    pub fn bool_setting(&self, key: &str, setting: &mut Option<bool>) {
101        if let Some(s) = self.content.get(key).and_then(Value::as_bool) {
102            *setting = Some(s)
103        }
104    }
105
106    pub fn u32_setting(&self, key: &str, setting: &mut Option<u32>) {
107        if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
108            *setting = Some(s as u32)
109        }
110    }
111
112    pub fn u64_setting(&self, key: &str, setting: &mut Option<u64>) {
113        if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
114            *setting = Some(s)
115        }
116    }
117
118    pub fn usize_setting(&self, key: &str, setting: &mut Option<usize>) {
119        if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
120            *setting = Some(s.try_into().unwrap())
121        }
122    }
123
124    pub fn f32_setting(&self, key: &str, setting: &mut Option<f32>) {
125        if let Some(s) = self.content.get(key).and_then(Value::as_f64) {
126            *setting = Some(s as f32)
127        }
128    }
129
130    pub fn from_f32_setting<T: From<f32>>(&self, key: &str, setting: &mut Option<T>) {
131        if let Some(s) = self.content.get(key).and_then(Value::as_f64) {
132            *setting = Some(T::from(s as f32))
133        }
134    }
135
136    pub fn enum_setting<T>(
137        &self,
138        key: &str,
139        setting: &mut Option<T>,
140        f: impl FnOnce(&str) -> Option<T>,
141    ) {
142        if let Some(s) = self.content.get(key).and_then(Value::as_str).and_then(f) {
143            *setting = Some(s)
144        }
145    }
146
147    pub fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
148        self.content.get(key).and_then(Value::as_str).and_then(f)
149    }
150
151    pub fn font_family_setting(
152        &self,
153        key: &str,
154        font_family: &mut Option<FontFamilyName>,
155        font_fallbacks: &mut Option<Vec<FontFamilyName>>,
156    ) {
157        let Some(css_name) = self.content.get(key).and_then(Value::as_str) else {
158            return;
159        };
160
161        let mut name_buffer = String::new();
162        let mut quote_char: Option<char> = None;
163        let mut fonts = Vec::new();
164        let mut add_font = |buffer: &mut String| {
165            let trimmed = buffer.trim();
166            if !trimmed.is_empty() {
167                fonts.push(trimmed.to_string().into());
168            }
169
170            buffer.clear();
171        };
172
173        for ch in css_name.chars() {
174            match (ch, quote_char) {
175                ('"' | '\'', None) => {
176                    quote_char = Some(ch);
177                }
178                (_, Some(q)) if ch == q => {
179                    quote_char = None;
180                }
181                (',', None) => {
182                    add_font(&mut name_buffer);
183                }
184                _ => {
185                    name_buffer.push(ch);
186                }
187            }
188        }
189
190        add_font(&mut name_buffer);
191
192        let mut iter = fonts.into_iter();
193        *font_family = iter.next();
194        let fallbacks: Vec<_> = iter.collect();
195        if !fallbacks.is_empty() {
196            *font_fallbacks = Some(fallbacks);
197        }
198    }
199}