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