settings.rs

  1mod font_size;
  2mod keymap_file;
  3mod settings_file;
  4mod settings_store;
  5
  6use anyhow::{bail, Result};
  7use gpui::{
  8    font_cache::{FamilyId, FontCache},
  9    fonts, AppContext, AssetSource,
 10};
 11use schemars::{
 12    gen::SchemaGenerator,
 13    schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
 14    JsonSchema,
 15};
 16use serde::{Deserialize, Serialize};
 17use serde_json::Value;
 18use sqlez::{
 19    bindable::{Bind, Column, StaticColumnCount},
 20    statement::Statement,
 21};
 22use std::{borrow::Cow, str, sync::Arc};
 23use theme::{Theme, ThemeRegistry};
 24use util::ResultExt as _;
 25
 26pub use font_size::{adjust_font_size_delta, font_size_for_setting};
 27pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
 28pub use settings_file::*;
 29pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
 30
 31pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
 32pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
 33
 34#[derive(Clone)]
 35pub struct Settings {
 36    pub buffer_font_family_name: String,
 37    pub buffer_font_features: fonts::Features,
 38    pub buffer_font_family: FamilyId,
 39    pub buffer_font_size: f32,
 40    pub active_pane_magnification: f32,
 41    pub confirm_quit: bool,
 42    pub show_call_status_icon: bool,
 43    pub autosave: Autosave,
 44    pub default_dock_anchor: DockAnchor,
 45    pub git: GitSettings,
 46    pub git_overrides: GitSettings,
 47    pub theme: Arc<Theme>,
 48    pub base_keymap: BaseKeymap,
 49}
 50
 51impl Setting for Settings {
 52    const KEY: Option<&'static str> = None;
 53
 54    type FileContent = SettingsFileContent;
 55
 56    fn load(
 57        defaults: &Self::FileContent,
 58        user_values: &[&Self::FileContent],
 59        cx: &AppContext,
 60    ) -> Result<Self> {
 61        let buffer_font_features = defaults.buffer_font_features.clone().unwrap();
 62        let themes = cx.global::<Arc<ThemeRegistry>>();
 63
 64        let mut this = Self {
 65            buffer_font_family: cx
 66                .font_cache()
 67                .load_family(
 68                    &[defaults.buffer_font_family.as_ref().unwrap()],
 69                    &buffer_font_features,
 70                )
 71                .unwrap(),
 72            buffer_font_family_name: defaults.buffer_font_family.clone().unwrap(),
 73            buffer_font_features,
 74            buffer_font_size: defaults.buffer_font_size.unwrap(),
 75            active_pane_magnification: defaults.active_pane_magnification.unwrap(),
 76            confirm_quit: defaults.confirm_quit.unwrap(),
 77            show_call_status_icon: defaults.show_call_status_icon.unwrap(),
 78            autosave: defaults.autosave.unwrap(),
 79            default_dock_anchor: defaults.default_dock_anchor.unwrap(),
 80            git: defaults.git.unwrap(),
 81            git_overrides: Default::default(),
 82            theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
 83            base_keymap: Default::default(),
 84        };
 85
 86        for value in user_values.into_iter().copied().cloned() {
 87            this.set_user_settings(value, themes.as_ref(), cx.font_cache());
 88        }
 89
 90        Ok(this)
 91    }
 92
 93    fn json_schema(
 94        generator: &mut SchemaGenerator,
 95        params: &SettingsJsonSchemaParams,
 96    ) -> schemars::schema::RootSchema {
 97        let mut root_schema = generator.root_schema_for::<SettingsFileContent>();
 98
 99        // Create a schema for a theme name.
100        let theme_name_schema = SchemaObject {
101            instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
102            enum_values: Some(
103                params
104                    .theme_names
105                    .iter()
106                    .cloned()
107                    .map(Value::String)
108                    .collect(),
109            ),
110            ..Default::default()
111        };
112
113        // Create a schema for a 'languages overrides' object, associating editor
114        // settings with specific langauges.
115        assert!(root_schema.definitions.contains_key("EditorSettings"));
116
117        let languages_object_schema = SchemaObject {
118            instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
119            object: Some(Box::new(ObjectValidation {
120                properties: params
121                    .language_names
122                    .iter()
123                    .map(|name| {
124                        (
125                            name.clone(),
126                            Schema::new_ref("#/definitions/EditorSettings".into()),
127                        )
128                    })
129                    .collect(),
130                ..Default::default()
131            })),
132            ..Default::default()
133        };
134
135        // Add these new schemas as definitions, and modify properties of the root
136        // schema to reference them.
137        root_schema.definitions.extend([
138            ("ThemeName".into(), theme_name_schema.into()),
139            ("Languages".into(), languages_object_schema.into()),
140        ]);
141        let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
142
143        root_schema_object.properties.extend([
144            (
145                "theme".to_owned(),
146                Schema::new_ref("#/definitions/ThemeName".into()),
147            ),
148            (
149                "languages".to_owned(),
150                Schema::new_ref("#/definitions/Languages".into()),
151            ),
152            // For backward compatibility
153            (
154                "language_overrides".to_owned(),
155                Schema::new_ref("#/definitions/Languages".into()),
156            ),
157        ]);
158
159        root_schema
160    }
161}
162
163#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
164pub enum BaseKeymap {
165    #[default]
166    VSCode,
167    JetBrains,
168    SublimeText,
169    Atom,
170    TextMate,
171}
172
173impl BaseKeymap {
174    pub const OPTIONS: [(&'static str, Self); 5] = [
175        ("VSCode (Default)", Self::VSCode),
176        ("Atom", Self::Atom),
177        ("JetBrains", Self::JetBrains),
178        ("Sublime Text", Self::SublimeText),
179        ("TextMate", Self::TextMate),
180    ];
181
182    pub fn asset_path(&self) -> Option<&'static str> {
183        match self {
184            BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
185            BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
186            BaseKeymap::Atom => Some("keymaps/atom.json"),
187            BaseKeymap::TextMate => Some("keymaps/textmate.json"),
188            BaseKeymap::VSCode => None,
189        }
190    }
191
192    pub fn names() -> impl Iterator<Item = &'static str> {
193        Self::OPTIONS.iter().map(|(name, _)| *name)
194    }
195
196    pub fn from_names(option: &str) -> BaseKeymap {
197        Self::OPTIONS
198            .iter()
199            .copied()
200            .find_map(|(name, value)| (name == option).then(|| value))
201            .unwrap_or_default()
202    }
203}
204#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
205pub struct GitSettings {
206    pub git_gutter: Option<GitGutter>,
207    pub gutter_debounce: Option<u64>,
208}
209
210#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
211#[serde(rename_all = "snake_case")]
212pub enum GitGutter {
213    #[default]
214    TrackedFiles,
215    Hide,
216}
217
218#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
219#[serde(rename_all = "snake_case")]
220pub enum Autosave {
221    Off,
222    AfterDelay { milliseconds: u64 },
223    OnFocusChange,
224    OnWindowChange,
225}
226
227#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
228#[serde(rename_all = "snake_case")]
229pub enum DockAnchor {
230    #[default]
231    Bottom,
232    Right,
233    Expanded,
234}
235
236impl StaticColumnCount for DockAnchor {}
237impl Bind for DockAnchor {
238    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
239        match self {
240            DockAnchor::Bottom => "Bottom",
241            DockAnchor::Right => "Right",
242            DockAnchor::Expanded => "Expanded",
243        }
244        .bind(statement, start_index)
245    }
246}
247
248impl Column for DockAnchor {
249    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
250        String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
251            Ok((
252                match anchor_text.as_ref() {
253                    "Bottom" => DockAnchor::Bottom,
254                    "Right" => DockAnchor::Right,
255                    "Expanded" => DockAnchor::Expanded,
256                    _ => bail!("Stored dock anchor is incorrect"),
257                },
258                next_index,
259            ))
260        })
261    }
262}
263
264#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
265pub struct SettingsFileContent {
266    #[serde(default)]
267    pub buffer_font_family: Option<String>,
268    #[serde(default)]
269    pub buffer_font_size: Option<f32>,
270    #[serde(default)]
271    pub buffer_font_features: Option<fonts::Features>,
272    #[serde(default)]
273    pub active_pane_magnification: Option<f32>,
274    #[serde(default)]
275    pub cursor_blink: Option<bool>,
276    #[serde(default)]
277    pub confirm_quit: Option<bool>,
278    #[serde(default)]
279    pub hover_popover_enabled: Option<bool>,
280    #[serde(default)]
281    pub show_completions_on_input: Option<bool>,
282    #[serde(default)]
283    pub show_call_status_icon: Option<bool>,
284    #[serde(default)]
285    pub autosave: Option<Autosave>,
286    #[serde(default)]
287    pub default_dock_anchor: Option<DockAnchor>,
288    #[serde(default)]
289    pub git: Option<GitSettings>,
290    #[serde(default)]
291    pub theme: Option<String>,
292    #[serde(default)]
293    pub base_keymap: Option<BaseKeymap>,
294}
295
296impl Settings {
297    pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
298        match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
299            Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
300            Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
301        }
302    }
303
304    /// Fill out the settings corresponding to the default.json file, overrides will be set later
305    pub fn defaults(
306        assets: impl AssetSource,
307        font_cache: &FontCache,
308        themes: &ThemeRegistry,
309    ) -> Self {
310        let defaults: SettingsFileContent = settings_store::parse_json_with_comments(
311            str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
312        )
313        .unwrap();
314
315        let buffer_font_features = defaults.buffer_font_features.unwrap();
316        Self {
317            buffer_font_family: font_cache
318                .load_family(
319                    &[defaults.buffer_font_family.as_ref().unwrap()],
320                    &buffer_font_features,
321                )
322                .unwrap(),
323            buffer_font_family_name: defaults.buffer_font_family.unwrap(),
324            buffer_font_features,
325            buffer_font_size: defaults.buffer_font_size.unwrap(),
326            active_pane_magnification: defaults.active_pane_magnification.unwrap(),
327            confirm_quit: defaults.confirm_quit.unwrap(),
328            show_call_status_icon: defaults.show_call_status_icon.unwrap(),
329            autosave: defaults.autosave.unwrap(),
330            default_dock_anchor: defaults.default_dock_anchor.unwrap(),
331            git: defaults.git.unwrap(),
332            git_overrides: Default::default(),
333            theme: themes.get(&defaults.theme.unwrap()).unwrap(),
334            base_keymap: Default::default(),
335        }
336    }
337
338    // Fill out the overrride and etc. settings from the user's settings.json
339    fn set_user_settings(
340        &mut self,
341        data: SettingsFileContent,
342        theme_registry: &ThemeRegistry,
343        font_cache: &FontCache,
344    ) {
345        let mut family_changed = false;
346        if let Some(value) = data.buffer_font_family {
347            self.buffer_font_family_name = value;
348            family_changed = true;
349        }
350        if let Some(value) = data.buffer_font_features {
351            self.buffer_font_features = value;
352            family_changed = true;
353        }
354        if family_changed {
355            if let Some(id) = font_cache
356                .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features)
357                .log_err()
358            {
359                self.buffer_font_family = id;
360            }
361        }
362
363        if let Some(value) = &data.theme {
364            if let Some(theme) = theme_registry.get(value).log_err() {
365                self.theme = theme;
366            }
367        }
368
369        merge(&mut self.buffer_font_size, data.buffer_font_size);
370        merge(
371            &mut self.active_pane_magnification,
372            data.active_pane_magnification,
373        );
374        merge(&mut self.confirm_quit, data.confirm_quit);
375        merge(&mut self.autosave, data.autosave);
376        merge(&mut self.default_dock_anchor, data.default_dock_anchor);
377        merge(&mut self.base_keymap, data.base_keymap);
378
379        self.git_overrides = data.git.unwrap_or_default();
380    }
381
382    pub fn git_gutter(&self) -> GitGutter {
383        self.git_overrides.git_gutter.unwrap_or_else(|| {
384            self.git
385                .git_gutter
386                .expect("git_gutter should be some by setting setup")
387        })
388    }
389
390    #[cfg(any(test, feature = "test-support"))]
391    pub fn test(cx: &gpui::AppContext) -> Settings {
392        Settings {
393            buffer_font_family_name: "Monaco".to_string(),
394            buffer_font_features: Default::default(),
395            buffer_font_family: cx
396                .font_cache()
397                .load_family(&["Monaco"], &Default::default())
398                .unwrap(),
399            buffer_font_size: 14.,
400            active_pane_magnification: 1.,
401            confirm_quit: false,
402            show_call_status_icon: true,
403            autosave: Autosave::Off,
404            default_dock_anchor: DockAnchor::Bottom,
405            git: Default::default(),
406            git_overrides: Default::default(),
407            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
408            base_keymap: Default::default(),
409        }
410    }
411
412    #[cfg(any(test, feature = "test-support"))]
413    pub fn test_async(cx: &mut gpui::TestAppContext) {
414        cx.update(|cx| {
415            let settings = Self::test(cx);
416            cx.set_global(settings);
417        });
418    }
419}
420
421fn merge<T: Copy>(target: &mut T, value: Option<T>) {
422    if let Some(value) = value {
423        *target = value;
424    }
425}