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