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