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