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