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