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