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