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