settings.rs

  1mod keymap_file;
  2
  3use anyhow::Result;
  4use gpui::font_cache::{FamilyId, FontCache};
  5use schemars::{
  6    gen::{SchemaGenerator, SchemaSettings},
  7    schema::{
  8        InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
  9    },
 10    JsonSchema,
 11};
 12use serde::{de::DeserializeOwned, Deserialize};
 13use serde_json::Value;
 14use std::{collections::HashMap, sync::Arc};
 15use theme::{Theme, ThemeRegistry};
 16use util::ResultExt as _;
 17
 18pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
 19
 20#[derive(Clone)]
 21pub struct Settings {
 22    pub buffer_font_family: FamilyId,
 23    pub buffer_font_size: f32,
 24    pub default_buffer_font_size: f32,
 25    pub vim_mode: bool,
 26    pub tab_size: u32,
 27    pub soft_wrap: SoftWrap,
 28    pub preferred_line_length: u32,
 29    pub format_on_save: bool,
 30    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
 31    pub theme: Arc<Theme>,
 32}
 33
 34#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
 35pub struct LanguageOverride {
 36    pub tab_size: Option<u32>,
 37    pub soft_wrap: Option<SoftWrap>,
 38    pub preferred_line_length: Option<u32>,
 39    pub format_on_save: Option<bool>,
 40}
 41
 42#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
 43#[serde(rename_all = "snake_case")]
 44pub enum SoftWrap {
 45    None,
 46    EditorWidth,
 47    PreferredLineLength,
 48}
 49
 50#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
 51pub struct SettingsFileContent {
 52    #[serde(default)]
 53    pub buffer_font_family: Option<String>,
 54    #[serde(default)]
 55    pub buffer_font_size: Option<f32>,
 56    #[serde(default)]
 57    pub vim_mode: Option<bool>,
 58    #[serde(default)]
 59    pub format_on_save: Option<bool>,
 60    #[serde(flatten)]
 61    pub editor: LanguageOverride,
 62    #[serde(default)]
 63    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
 64    #[serde(default)]
 65    pub theme: Option<String>,
 66}
 67
 68impl Settings {
 69    pub fn new(
 70        buffer_font_family: &str,
 71        font_cache: &FontCache,
 72        theme: Arc<Theme>,
 73    ) -> Result<Self> {
 74        Ok(Self {
 75            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
 76            buffer_font_size: 15.,
 77            default_buffer_font_size: 15.,
 78            vim_mode: false,
 79            tab_size: 4,
 80            soft_wrap: SoftWrap::None,
 81            preferred_line_length: 80,
 82            language_overrides: Default::default(),
 83            format_on_save: true,
 84            theme,
 85        })
 86    }
 87
 88    pub fn with_overrides(
 89        mut self,
 90        language_name: impl Into<Arc<str>>,
 91        overrides: LanguageOverride,
 92    ) -> Self {
 93        self.language_overrides
 94            .insert(language_name.into(), overrides);
 95        self
 96    }
 97
 98    pub fn tab_size(&self, language: Option<&str>) -> u32 {
 99        language
100            .and_then(|language| self.language_overrides.get(language))
101            .and_then(|settings| settings.tab_size)
102            .unwrap_or(self.tab_size)
103    }
104
105    pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
106        language
107            .and_then(|language| self.language_overrides.get(language))
108            .and_then(|settings| settings.soft_wrap)
109            .unwrap_or(self.soft_wrap)
110    }
111
112    pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
113        language
114            .and_then(|language| self.language_overrides.get(language))
115            .and_then(|settings| settings.preferred_line_length)
116            .unwrap_or(self.preferred_line_length)
117    }
118
119    pub fn format_on_save(&self, language: Option<&str>) -> bool {
120        language
121            .and_then(|language| self.language_overrides.get(language))
122            .and_then(|settings| settings.format_on_save)
123            .unwrap_or(self.format_on_save)
124    }
125
126    #[cfg(any(test, feature = "test-support"))]
127    pub fn test(cx: &gpui::AppContext) -> Settings {
128        Settings {
129            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
130            buffer_font_size: 14.,
131            default_buffer_font_size: 14.,
132            vim_mode: false,
133            tab_size: 4,
134            soft_wrap: SoftWrap::None,
135            preferred_line_length: 80,
136            format_on_save: true,
137            language_overrides: Default::default(),
138            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
139        }
140    }
141
142    #[cfg(any(test, feature = "test-support"))]
143    pub fn test_async(cx: &mut gpui::TestAppContext) {
144        cx.update(|cx| {
145            let settings = Self::test(cx);
146            cx.set_global(settings.clone());
147        });
148    }
149
150    pub fn merge(
151        &mut self,
152        data: &SettingsFileContent,
153        theme_registry: &ThemeRegistry,
154        font_cache: &FontCache,
155    ) {
156        if let Some(value) = &data.buffer_font_family {
157            if let Some(id) = font_cache.load_family(&[value]).log_err() {
158                self.buffer_font_family = id;
159            }
160        }
161        if let Some(value) = &data.theme {
162            if let Some(theme) = theme_registry.get(&value.to_string()).log_err() {
163                self.theme = theme;
164            }
165        }
166
167        merge(&mut self.buffer_font_size, data.buffer_font_size);
168        merge(&mut self.default_buffer_font_size, data.buffer_font_size);
169        merge(&mut self.vim_mode, data.vim_mode);
170        merge(&mut self.format_on_save, data.format_on_save);
171        merge(&mut self.soft_wrap, data.editor.soft_wrap);
172        merge(&mut self.tab_size, data.editor.tab_size);
173        merge(
174            &mut self.preferred_line_length,
175            data.editor.preferred_line_length,
176        );
177
178        for (language_name, settings) in data.language_overrides.clone().into_iter() {
179            let target = self
180                .language_overrides
181                .entry(language_name.into())
182                .or_default();
183
184            merge_option(&mut target.tab_size, settings.tab_size);
185            merge_option(&mut target.soft_wrap, settings.soft_wrap);
186            merge_option(&mut target.format_on_save, settings.format_on_save);
187            merge_option(
188                &mut target.preferred_line_length,
189                settings.preferred_line_length,
190            );
191        }
192    }
193}
194
195pub fn settings_file_json_schema(
196    theme_names: Vec<String>,
197    language_names: Vec<String>,
198) -> serde_json::Value {
199    let settings = SchemaSettings::draft07().with(|settings| {
200        settings.option_add_null_type = false;
201    });
202    let generator = SchemaGenerator::new(settings);
203    let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
204
205    // Construct theme names reference type
206    let theme_names = theme_names
207        .into_iter()
208        .map(|name| Value::String(name))
209        .collect();
210    let theme_names_schema = Schema::Object(SchemaObject {
211        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
212        enum_values: Some(theme_names),
213        ..Default::default()
214    });
215    root_schema
216        .definitions
217        .insert("ThemeName".to_owned(), theme_names_schema);
218
219    // Construct language overrides reference type
220    let language_override_schema_reference = Schema::Object(SchemaObject {
221        reference: Some("#/definitions/LanguageOverride".to_owned()),
222        ..Default::default()
223    });
224    let language_overrides_properties = language_names
225        .into_iter()
226        .map(|name| {
227            (
228                name,
229                Schema::Object(SchemaObject {
230                    subschemas: Some(Box::new(SubschemaValidation {
231                        all_of: Some(vec![language_override_schema_reference.clone()]),
232                        ..Default::default()
233                    })),
234                    ..Default::default()
235                }),
236            )
237        })
238        .collect();
239    let language_overrides_schema = Schema::Object(SchemaObject {
240        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
241        object: Some(Box::new(ObjectValidation {
242            properties: language_overrides_properties,
243            ..Default::default()
244        })),
245        ..Default::default()
246    });
247    root_schema
248        .definitions
249        .insert("LanguageOverrides".to_owned(), language_overrides_schema);
250
251    // Modify theme property to use new theme reference type
252    let settings_file_schema = root_schema.schema.object.as_mut().unwrap();
253    let language_overrides_schema_reference = Schema::Object(SchemaObject {
254        reference: Some("#/definitions/ThemeName".to_owned()),
255        ..Default::default()
256    });
257    settings_file_schema.properties.insert(
258        "theme".to_owned(),
259        Schema::Object(SchemaObject {
260            subschemas: Some(Box::new(SubschemaValidation {
261                all_of: Some(vec![language_overrides_schema_reference]),
262                ..Default::default()
263            })),
264            ..Default::default()
265        }),
266    );
267
268    // Modify language_overrides property to use LanguageOverrides reference
269    settings_file_schema.properties.insert(
270        "language_overrides".to_owned(),
271        Schema::Object(SchemaObject {
272            reference: Some("#/definitions/LanguageOverrides".to_owned()),
273            ..Default::default()
274        }),
275    );
276    serde_json::to_value(root_schema).unwrap()
277}
278
279fn merge<T: Copy>(target: &mut T, value: Option<T>) {
280    if let Some(value) = value {
281        *target = value;
282    }
283}
284
285fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
286    if value.is_some() {
287        *target = value;
288    }
289}
290
291pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
292    Ok(serde_json::from_reader(
293        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
294    )?)
295}