settings.rs

  1mod keymap_file;
  2
  3use anyhow::Result;
  4use gpui::{
  5    font_cache::{FamilyId, FontCache},
  6    AssetSource,
  7};
  8use schemars::{
  9    gen::{SchemaGenerator, SchemaSettings},
 10    schema::{
 11        InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
 12    },
 13    JsonSchema,
 14};
 15use serde::{de::DeserializeOwned, Deserialize};
 16use serde_json::Value;
 17use std::{collections::HashMap, num::NonZeroU32, sync::Arc};
 18use theme::{Theme, ThemeRegistry};
 19use util::ResultExt as _;
 20
 21pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
 22
 23#[derive(Clone)]
 24pub struct Settings {
 25    pub projects_online_by_default: bool,
 26    pub buffer_font_family: FamilyId,
 27    pub buffer_font_size: f32,
 28    pub default_buffer_font_size: f32,
 29    pub hover_popover_enabled: bool,
 30    pub vim_mode: bool,
 31    pub autosave: Autosave,
 32    pub language_settings: LanguageSettings,
 33    pub language_defaults: HashMap<Arc<str>, LanguageSettings>,
 34    pub language_overrides: HashMap<Arc<str>, LanguageSettings>,
 35    pub theme: Arc<Theme>,
 36}
 37
 38#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
 39pub struct LanguageSettings {
 40    pub tab_size: Option<NonZeroU32>,
 41    pub hard_tabs: Option<bool>,
 42    pub soft_wrap: Option<SoftWrap>,
 43    pub preferred_line_length: Option<u32>,
 44    pub format_on_save: Option<FormatOnSave>,
 45    pub enable_language_server: Option<bool>,
 46}
 47
 48#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
 49#[serde(rename_all = "snake_case")]
 50pub enum SoftWrap {
 51    None,
 52    EditorWidth,
 53    PreferredLineLength,
 54}
 55
 56#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
 57#[serde(rename_all = "snake_case")]
 58pub enum FormatOnSave {
 59    Off,
 60    LanguageServer,
 61    External {
 62        command: String,
 63        arguments: Vec<String>,
 64    },
 65}
 66
 67#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
 68#[serde(rename_all = "snake_case")]
 69pub enum Autosave {
 70    Off,
 71    AfterDelay { milliseconds: u64 },
 72    OnFocusChange,
 73    OnWindowChange,
 74}
 75
 76#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
 77pub struct SettingsFileContent {
 78    #[serde(default)]
 79    pub projects_online_by_default: Option<bool>,
 80    #[serde(default)]
 81    pub buffer_font_family: Option<String>,
 82    #[serde(default)]
 83    pub buffer_font_size: Option<f32>,
 84    #[serde(default)]
 85    pub hover_popover_enabled: Option<bool>,
 86    #[serde(default)]
 87    pub vim_mode: Option<bool>,
 88    #[serde(default)]
 89    pub format_on_save: Option<FormatOnSave>,
 90    #[serde(default)]
 91    pub autosave: Option<Autosave>,
 92    #[serde(default)]
 93    pub enable_language_server: Option<bool>,
 94    #[serde(flatten)]
 95    pub editor: LanguageSettings,
 96    #[serde(default)]
 97    pub language_overrides: HashMap<Arc<str>, LanguageSettings>,
 98    #[serde(default)]
 99    pub theme: Option<String>,
100}
101
102impl Settings {
103    pub fn defaults(
104        assets: impl AssetSource,
105        font_cache: &FontCache,
106        themes: &ThemeRegistry,
107    ) -> Self {
108        let defaults = assets.load("default-settings.json").unwrap();
109        let defaults: SettingsFileContent = serde_json::from_slice(defaults.as_ref()).unwrap();
110        Self {
111            buffer_font_family: font_cache
112                .load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
113                .unwrap(),
114            buffer_font_size: defaults.buffer_font_size.unwrap(),
115            default_buffer_font_size: defaults.buffer_font_size.unwrap(),
116            hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
117            projects_online_by_default: defaults.projects_online_by_default.unwrap(),
118            vim_mode: defaults.vim_mode.unwrap(),
119            autosave: defaults.autosave.unwrap(),
120            language_settings: LanguageSettings {
121                tab_size: defaults.editor.tab_size,
122                hard_tabs: defaults.editor.hard_tabs,
123                soft_wrap: defaults.editor.soft_wrap,
124                preferred_line_length: defaults.editor.preferred_line_length,
125                format_on_save: defaults.editor.format_on_save,
126                enable_language_server: defaults.editor.enable_language_server,
127            },
128            language_defaults: defaults.language_overrides,
129            language_overrides: Default::default(),
130            theme: themes.get(&defaults.theme.unwrap()).unwrap(),
131        }
132    }
133
134    pub fn with_language_defaults(
135        mut self,
136        language_name: impl Into<Arc<str>>,
137        overrides: LanguageSettings,
138    ) -> Self {
139        self.language_defaults
140            .insert(language_name.into(), overrides);
141        self
142    }
143
144    pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
145        self.language_setting(language, |settings| settings.tab_size)
146            .unwrap_or(4.try_into().unwrap())
147    }
148
149    pub fn hard_tabs(&self, language: Option<&str>) -> bool {
150        self.language_setting(language, |settings| settings.hard_tabs)
151            .unwrap_or(false)
152    }
153
154    pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
155        self.language_setting(language, |settings| settings.soft_wrap)
156            .unwrap_or(SoftWrap::None)
157    }
158
159    pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
160        self.language_setting(language, |settings| settings.preferred_line_length)
161            .unwrap_or(80)
162    }
163
164    pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
165        self.language_setting(language, |settings| settings.format_on_save.clone())
166            .unwrap_or(FormatOnSave::LanguageServer)
167    }
168
169    pub fn enable_language_server(&self, language: Option<&str>) -> bool {
170        self.language_setting(language, |settings| settings.enable_language_server)
171            .unwrap_or(true)
172    }
173
174    fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> Option<R>
175    where
176        F: Fn(&LanguageSettings) -> Option<R>,
177    {
178        let mut language_override = None;
179        let mut language_default = None;
180        if let Some(language) = language {
181            language_override = self.language_overrides.get(language).and_then(&f);
182            language_default = self.language_defaults.get(language).and_then(&f);
183        }
184
185        language_override
186            .or_else(|| f(&self.language_settings))
187            .or(language_default)
188    }
189
190    #[cfg(any(test, feature = "test-support"))]
191    pub fn test(cx: &gpui::AppContext) -> Settings {
192        Settings {
193            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
194            buffer_font_size: 14.,
195            default_buffer_font_size: 14.,
196            hover_popover_enabled: true,
197            vim_mode: false,
198            autosave: Autosave::Off,
199            language_settings: Default::default(),
200            language_defaults: Default::default(),
201            language_overrides: Default::default(),
202            projects_online_by_default: true,
203            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
204        }
205    }
206
207    #[cfg(any(test, feature = "test-support"))]
208    pub fn test_async(cx: &mut gpui::TestAppContext) {
209        cx.update(|cx| {
210            let settings = Self::test(cx);
211            cx.set_global(settings.clone());
212        });
213    }
214
215    pub fn merge(
216        &mut self,
217        data: &SettingsFileContent,
218        theme_registry: &ThemeRegistry,
219        font_cache: &FontCache,
220    ) {
221        if let Some(value) = &data.buffer_font_family {
222            if let Some(id) = font_cache.load_family(&[value]).log_err() {
223                self.buffer_font_family = id;
224            }
225        }
226        if let Some(value) = &data.theme {
227            if let Some(theme) = theme_registry.get(&value.to_string()).log_err() {
228                self.theme = theme;
229            }
230        }
231
232        merge(
233            &mut self.projects_online_by_default,
234            data.projects_online_by_default,
235        );
236        merge(&mut self.buffer_font_size, data.buffer_font_size);
237        merge(&mut self.default_buffer_font_size, data.buffer_font_size);
238        merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
239        merge(&mut self.vim_mode, data.vim_mode);
240        merge(&mut self.autosave, data.autosave);
241        merge_option(
242            &mut self.language_settings.format_on_save,
243            data.format_on_save.clone(),
244        );
245        merge_option(
246            &mut self.language_settings.enable_language_server,
247            data.enable_language_server,
248        );
249        merge_option(&mut self.language_settings.soft_wrap, data.editor.soft_wrap);
250        merge_option(&mut self.language_settings.tab_size, data.editor.tab_size);
251        merge_option(
252            &mut self.language_settings.preferred_line_length,
253            data.editor.preferred_line_length,
254        );
255
256        for (language_name, settings) in data.language_overrides.clone().into_iter() {
257            let target = self
258                .language_overrides
259                .entry(language_name.into())
260                .or_default();
261
262            merge_option(&mut target.tab_size, settings.tab_size);
263            merge_option(&mut target.soft_wrap, settings.soft_wrap);
264            merge_option(&mut target.format_on_save, settings.format_on_save);
265            merge_option(
266                &mut target.enable_language_server,
267                settings.enable_language_server,
268            );
269            merge_option(
270                &mut target.preferred_line_length,
271                settings.preferred_line_length,
272            );
273        }
274    }
275}
276
277pub fn settings_file_json_schema(
278    theme_names: Vec<String>,
279    language_names: Vec<String>,
280) -> serde_json::Value {
281    let settings = SchemaSettings::draft07().with(|settings| {
282        settings.option_add_null_type = false;
283    });
284    let generator = SchemaGenerator::new(settings);
285    let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
286
287    // Construct theme names reference type
288    let theme_names = theme_names
289        .into_iter()
290        .map(|name| Value::String(name))
291        .collect();
292    let theme_names_schema = Schema::Object(SchemaObject {
293        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
294        enum_values: Some(theme_names),
295        ..Default::default()
296    });
297    root_schema
298        .definitions
299        .insert("ThemeName".to_owned(), theme_names_schema);
300
301    // Construct language settings reference type
302    let language_settings_schema_reference = Schema::Object(SchemaObject {
303        reference: Some("#/definitions/LanguageSettings".to_owned()),
304        ..Default::default()
305    });
306    let language_settings_properties = language_names
307        .into_iter()
308        .map(|name| {
309            (
310                name,
311                Schema::Object(SchemaObject {
312                    subschemas: Some(Box::new(SubschemaValidation {
313                        all_of: Some(vec![language_settings_schema_reference.clone()]),
314                        ..Default::default()
315                    })),
316                    ..Default::default()
317                }),
318            )
319        })
320        .collect();
321    let language_overrides_schema = Schema::Object(SchemaObject {
322        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
323        object: Some(Box::new(ObjectValidation {
324            properties: language_settings_properties,
325            ..Default::default()
326        })),
327        ..Default::default()
328    });
329    root_schema
330        .definitions
331        .insert("LanguageOverrides".to_owned(), language_overrides_schema);
332
333    // Modify theme property to use new theme reference type
334    let settings_file_schema = root_schema.schema.object.as_mut().unwrap();
335    let language_overrides_schema_reference = Schema::Object(SchemaObject {
336        reference: Some("#/definitions/ThemeName".to_owned()),
337        ..Default::default()
338    });
339    settings_file_schema.properties.insert(
340        "theme".to_owned(),
341        Schema::Object(SchemaObject {
342            subschemas: Some(Box::new(SubschemaValidation {
343                all_of: Some(vec![language_overrides_schema_reference]),
344                ..Default::default()
345            })),
346            ..Default::default()
347        }),
348    );
349
350    // Modify language_overrides property to use LanguageOverrides reference
351    settings_file_schema.properties.insert(
352        "language_overrides".to_owned(),
353        Schema::Object(SchemaObject {
354            reference: Some("#/definitions/LanguageOverrides".to_owned()),
355            ..Default::default()
356        }),
357    );
358    serde_json::to_value(root_schema).unwrap()
359}
360
361fn merge<T: Copy>(target: &mut T, value: Option<T>) {
362    if let Some(value) = value {
363        *target = value;
364    }
365}
366
367fn merge_option<T>(target: &mut Option<T>, value: Option<T>) {
368    if value.is_some() {
369        *target = value;
370    }
371}
372
373pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
374    Ok(serde_json::from_reader(
375        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
376    )?)
377}