Merge pull request #1331 from zed-industries/discoverable-settings

Max Brunsfeld created

Make settings more discoverable

Change summary

assets/settings/default.json           | 109 ++++++++++++
assets/settings/header-comments.json   |   8 
crates/collab/src/integration_tests.rs |   2 
crates/editor/src/display_map.rs       |   2 
crates/editor/src/editor.rs            |  12 
crates/project/src/project_tests.rs    |   6 
crates/search/src/buffer_search.rs     |   7 
crates/search/src/project_search.rs    |   7 
crates/settings/src/settings.rs        | 241 ++++++++++++++-------------
crates/theme/src/theme.rs              |   2 
crates/zed/src/main.rs                 |  70 -------
crates/zed/src/menus.rs                |   4 
crates/zed/src/settings_file.rs        |   4 
crates/zed/src/zed.rs                  | 118 +++++++++----
14 files changed, 348 insertions(+), 244 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -0,0 +1,109 @@
+{
+    // The name of the Zed theme to use for the UI
+    "theme": "cave-dark",
+
+    // The name of a font to use for rendering text in the editor
+    "buffer_font_family": "Zed Mono",
+
+    // The default font size for text in the editor
+    "buffer_font_size": 15,
+
+    // Whether to enable vim modes and key bindings
+    "vim_mode": false,
+
+    // Whether to show the informational hover box when moving the mouse
+    // over symbols in the editor.
+    "hover_popover_enabled": true,
+
+    // Whether new projects should start out 'online'. Online projects
+    // appear in the contacts panel under your name, so that your contacts
+    // can see which projects you are working on. Regardless of this
+    // setting, projects keep their last online status when you reopen them.
+    "projects_online_by_default": true,
+
+    // Whether to use language servers to provide code intelligence.
+    "enable_language_server": true,
+
+    // When to automatically save edited buffers. This setting can
+    // take four values.
+    //
+    // 1. Never automatically save:
+    //     "autosave": "off",
+    // 2. Save when changing focus away from the Zed window:
+    //     "autosave": "on_window_change",
+    // 3. Save when changing focus away from a specific buffer:
+    //     "autosave": "on_focus_change",
+    // 4. Save when idle for a certain amount of time:
+    //     "autosave": { "after_delay": {"milliseconds": 500} },
+    "autosave": "off",
+
+    // How to auto-format modified buffers when saving them. This
+    // setting can take three values:
+    //
+    // 1. Don't format code
+    //     "format_on_save": "off"
+    // 2. Format code using the current language server:
+    //     "format_on_save": "language_server"
+    // 3. Format code using an external command:
+    //     "format_on_save": {
+    //       "external": {
+    //         "command": "sed",
+    //         "arguments": ["-e", "s/ *$//"]
+    //       }
+    //     },
+    "format_on_save": "language_server",
+
+    // How to soft-wrap long lines of text. This setting can take
+    // three values:
+    //
+    // 1. Do not soft wrap.
+    //      "soft_wrap": "none",
+    // 2. Soft wrap lines that overflow the editor:
+    //      "soft_wrap": "editor_width",
+    // 2. Soft wrap lines at the preferred line length
+    //      "soft_wrap": "preferred_line_length",
+    "soft_wrap": "none",
+
+    // The column at which to soft-wrap lines, for buffers where soft-wrap
+    // is enabled.
+    "preferred_line_length": 80,
+
+    // Whether to indent lines using tab characters, as opposed to multiple
+    // spaces.
+    "hard_tabs": false,
+
+    // How many columns a tab should occupy.
+    "tab_size": 4,
+
+    // Different settings for specific languages.
+    "languages": {
+        "Plain Text": {
+            "soft_wrap": "preferred_line_length"
+        },
+        "C": {
+            "tab_size": 2
+        },
+        "C++": {
+            "tab_size": 2
+        },
+        "Go": {
+            "tab_size": 4,
+            "hard_tabs": true
+        },
+        "Markdown": {
+            "soft_wrap": "preferred_line_length"
+        },
+        "Rust": {
+            "tab_size": 4
+        },
+        "JavaScript": {
+            "tab_size": 2
+        },
+        "TypeScript": {
+            "tab_size": 2
+        },
+        "TSX": {
+            "tab_size": 2
+        }
+    }
+}

assets/settings/header-comments.json 🔗

@@ -0,0 +1,8 @@
+// Zed settings
+//
+// For information on how to configure Zed, see the Zed
+// documentation: https://zed.dev/docs/configuring-zed
+//
+// To see all of Zed's default settings without changing your
+// custom settings, run the `open default settings` command
+// from the command palette or from `Zed` application menu.

crates/collab/src/integration_tests.rs 🔗

@@ -2010,7 +2010,7 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
     // host's configuration is honored as opposed to using the guest's settings.
     cx_a.update(|cx| {
         cx.update_global(|settings: &mut Settings, _| {
-            settings.language_settings.format_on_save = Some(FormatOnSave::External {
+            settings.editor_defaults.format_on_save = Some(FormatOnSave::External {
                 command: "awk".to_string(),
                 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
             });

crates/editor/src/display_map.rs 🔗

@@ -983,7 +983,7 @@ pub mod tests {
         language.set_theme(&theme);
         cx.update(|cx| {
             let mut settings = Settings::test(cx);
-            settings.language_settings.tab_size = Some(2.try_into().unwrap());
+            settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
             cx.set_global(settings);
         });
 

crates/editor/src/editor.rs 🔗

@@ -6236,7 +6236,7 @@ mod tests {
     use language::{FakeLspAdapter, LanguageConfig};
     use lsp::FakeLanguageServer;
     use project::FakeFs;
-    use settings::LanguageSettings;
+    use settings::EditorSettings;
     use std::{cell::RefCell, rc::Rc, time::Instant};
     use text::Point;
     use unindent::Unindent;
@@ -7613,7 +7613,7 @@ mod tests {
         let mut cx = EditorTestContext::new(cx).await;
         cx.update(|cx| {
             cx.update_global::<Settings, _, _>(|settings, _| {
-                settings.language_settings.hard_tabs = Some(true);
+                settings.editor_overrides.hard_tabs = Some(true);
             });
         });
 
@@ -7696,14 +7696,14 @@ mod tests {
             Settings::test(cx)
                 .with_language_defaults(
                     "TOML",
-                    LanguageSettings {
+                    EditorSettings {
                         tab_size: Some(2.try_into().unwrap()),
                         ..Default::default()
                     },
                 )
                 .with_language_defaults(
                     "Rust",
-                    LanguageSettings {
+                    EditorSettings {
                         tab_size: Some(4.try_into().unwrap()),
                         ..Default::default()
                     },
@@ -9380,7 +9380,7 @@ mod tests {
             cx.update_global::<Settings, _, _>(|settings, _| {
                 settings.language_overrides.insert(
                     "Rust".into(),
-                    LanguageSettings {
+                    EditorSettings {
                         tab_size: Some(8.try_into().unwrap()),
                         ..Default::default()
                     },
@@ -9496,7 +9496,7 @@ mod tests {
             cx.update_global::<Settings, _, _>(|settings, _| {
                 settings.language_overrides.insert(
                     "Rust".into(),
-                    LanguageSettings {
+                    EditorSettings {
                         tab_size: Some(8.try_into().unwrap()),
                         ..Default::default()
                     },

crates/project/src/project_tests.rs 🔗

@@ -883,7 +883,7 @@ async fn test_toggling_enable_language_server(
         cx.update_global(|settings: &mut Settings, _| {
             settings.language_overrides.insert(
                 Arc::from("Rust"),
-                settings::LanguageSettings {
+                settings::EditorSettings {
                     enable_language_server: Some(false),
                     ..Default::default()
                 },
@@ -900,14 +900,14 @@ async fn test_toggling_enable_language_server(
         cx.update_global(|settings: &mut Settings, _| {
             settings.language_overrides.insert(
                 Arc::from("Rust"),
-                settings::LanguageSettings {
+                settings::EditorSettings {
                     enable_language_server: Some(true),
                     ..Default::default()
                 },
             );
             settings.language_overrides.insert(
                 Arc::from("JavaScript"),
-                settings::LanguageSettings {
+                settings::EditorSettings {
                     enable_language_server: Some(false),
                     ..Default::default()
                 },

crates/search/src/buffer_search.rs 🔗

@@ -608,8 +608,11 @@ mod tests {
         let fonts = cx.font_cache();
         let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
         theme.search.match_background = Color::red();
-        let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
-        cx.update(|cx| cx.set_global(settings));
+        cx.update(|cx| {
+            let mut settings = Settings::test(cx);
+            settings.theme = Arc::new(theme);
+            cx.set_global(settings)
+        });
 
         let buffer = cx.add_model(|cx| {
             Buffer::new(

crates/search/src/project_search.rs 🔗

@@ -911,8 +911,11 @@ mod tests {
         let fonts = cx.font_cache();
         let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
         theme.search.match_background = Color::red();
-        let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
-        cx.update(|cx| cx.set_global(settings));
+        cx.update(|cx| {
+            let mut settings = Settings::test(cx);
+            settings.theme = Arc::new(theme);
+            cx.set_global(settings)
+        });
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(

crates/settings/src/settings.rs 🔗

@@ -1,17 +1,18 @@
 mod keymap_file;
 
 use anyhow::Result;
-use gpui::font_cache::{FamilyId, FontCache};
+use gpui::{
+    font_cache::{FamilyId, FontCache},
+    AssetSource,
+};
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
-    schema::{
-        InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
-    },
+    schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
     JsonSchema,
 };
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
-use std::{collections::HashMap, num::NonZeroU32, sync::Arc};
+use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc};
 use theme::{Theme, ThemeRegistry};
 use util::ResultExt as _;
 
@@ -26,14 +27,15 @@ pub struct Settings {
     pub hover_popover_enabled: bool,
     pub vim_mode: bool,
     pub autosave: Autosave,
-    pub language_settings: LanguageSettings,
-    pub language_defaults: HashMap<Arc<str>, LanguageSettings>,
-    pub language_overrides: HashMap<Arc<str>, LanguageSettings>,
+    pub editor_defaults: EditorSettings,
+    pub editor_overrides: EditorSettings,
+    pub language_defaults: HashMap<Arc<str>, EditorSettings>,
+    pub language_overrides: HashMap<Arc<str>, EditorSettings>,
     pub theme: Arc<Theme>,
 }
 
 #[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
-pub struct LanguageSettings {
+pub struct EditorSettings {
     pub tab_size: Option<NonZeroU32>,
     pub hard_tabs: Option<bool>,
     pub soft_wrap: Option<SoftWrap>,
@@ -83,44 +85,61 @@ pub struct SettingsFileContent {
     #[serde(default)]
     pub vim_mode: Option<bool>,
     #[serde(default)]
-    pub format_on_save: Option<FormatOnSave>,
-    #[serde(default)]
     pub autosave: Option<Autosave>,
-    #[serde(default)]
-    pub enable_language_server: Option<bool>,
     #[serde(flatten)]
-    pub editor: LanguageSettings,
+    pub editor: EditorSettings,
     #[serde(default)]
-    pub language_overrides: HashMap<Arc<str>, LanguageSettings>,
+    #[serde(alias = "language_overrides")]
+    pub languages: HashMap<Arc<str>, EditorSettings>,
     #[serde(default)]
     pub theme: Option<String>,
 }
 
 impl Settings {
-    pub fn new(
-        buffer_font_family: &str,
+    pub fn defaults(
+        assets: impl AssetSource,
         font_cache: &FontCache,
-        theme: Arc<Theme>,
-    ) -> Result<Self> {
-        Ok(Self {
-            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
-            buffer_font_size: 15.,
-            default_buffer_font_size: 15.,
-            hover_popover_enabled: true,
-            vim_mode: false,
-            autosave: Autosave::Off,
-            language_settings: Default::default(),
-            language_defaults: Default::default(),
+        themes: &ThemeRegistry,
+    ) -> Self {
+        fn required<T>(value: Option<T>) -> Option<T> {
+            assert!(value.is_some(), "missing default setting value");
+            value
+        }
+
+        let defaults: SettingsFileContent = parse_json_with_comments(
+            str::from_utf8(assets.load("settings/default.json").unwrap().as_ref()).unwrap(),
+        )
+        .unwrap();
+
+        Self {
+            buffer_font_family: font_cache
+                .load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
+                .unwrap(),
+            buffer_font_size: defaults.buffer_font_size.unwrap(),
+            default_buffer_font_size: defaults.buffer_font_size.unwrap(),
+            hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
+            projects_online_by_default: defaults.projects_online_by_default.unwrap(),
+            vim_mode: defaults.vim_mode.unwrap(),
+            autosave: defaults.autosave.unwrap(),
+            editor_defaults: EditorSettings {
+                tab_size: required(defaults.editor.tab_size),
+                hard_tabs: required(defaults.editor.hard_tabs),
+                soft_wrap: required(defaults.editor.soft_wrap),
+                preferred_line_length: required(defaults.editor.preferred_line_length),
+                format_on_save: required(defaults.editor.format_on_save),
+                enable_language_server: required(defaults.editor.enable_language_server),
+            },
+            language_defaults: defaults.languages,
+            editor_overrides: Default::default(),
             language_overrides: Default::default(),
-            projects_online_by_default: true,
-            theme,
-        })
+            theme: themes.get(&defaults.theme.unwrap()).unwrap(),
+        }
     }
 
     pub fn with_language_defaults(
         mut self,
         language_name: impl Into<Arc<str>>,
-        overrides: LanguageSettings,
+        overrides: EditorSettings,
     ) -> Self {
         self.language_defaults
             .insert(language_name.into(), overrides);
@@ -129,48 +148,37 @@ impl Settings {
 
     pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
         self.language_setting(language, |settings| settings.tab_size)
-            .unwrap_or(4.try_into().unwrap())
     }
 
     pub fn hard_tabs(&self, language: Option<&str>) -> bool {
         self.language_setting(language, |settings| settings.hard_tabs)
-            .unwrap_or(false)
     }
 
     pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
         self.language_setting(language, |settings| settings.soft_wrap)
-            .unwrap_or(SoftWrap::None)
     }
 
     pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
         self.language_setting(language, |settings| settings.preferred_line_length)
-            .unwrap_or(80)
     }
 
     pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
         self.language_setting(language, |settings| settings.format_on_save.clone())
-            .unwrap_or(FormatOnSave::LanguageServer)
     }
 
     pub fn enable_language_server(&self, language: Option<&str>) -> bool {
         self.language_setting(language, |settings| settings.enable_language_server)
-            .unwrap_or(true)
     }
 
-    fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> Option<R>
+    fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> R
     where
-        F: Fn(&LanguageSettings) -> Option<R>,
+        F: Fn(&EditorSettings) -> Option<R>,
     {
-        let mut language_override = None;
-        let mut language_default = None;
-        if let Some(language) = language {
-            language_override = self.language_overrides.get(language).and_then(&f);
-            language_default = self.language_defaults.get(language).and_then(&f);
-        }
-
-        language_override
-            .or_else(|| f(&self.language_settings))
-            .or(language_default)
+        None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f)))
+            .or_else(|| f(&self.editor_overrides))
+            .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f)))
+            .or_else(|| f(&self.editor_defaults))
+            .expect("missing default")
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -182,7 +190,15 @@ impl Settings {
             hover_popover_enabled: true,
             vim_mode: false,
             autosave: Autosave::Off,
-            language_settings: Default::default(),
+            editor_defaults: EditorSettings {
+                tab_size: Some(4.try_into().unwrap()),
+                hard_tabs: Some(false),
+                soft_wrap: Some(SoftWrap::None),
+                preferred_line_length: Some(80),
+                format_on_save: Some(FormatOnSave::LanguageServer),
+                enable_language_server: Some(true),
+            },
+            editor_overrides: Default::default(),
             language_defaults: Default::default(),
             language_overrides: Default::default(),
             projects_online_by_default: true,
@@ -224,22 +240,23 @@ impl Settings {
         merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
         merge(&mut self.vim_mode, data.vim_mode);
         merge(&mut self.autosave, data.autosave);
+
         merge_option(
-            &mut self.language_settings.format_on_save,
-            data.format_on_save.clone(),
+            &mut self.editor_overrides.format_on_save,
+            data.editor.format_on_save.clone(),
         );
         merge_option(
-            &mut self.language_settings.enable_language_server,
-            data.enable_language_server,
+            &mut self.editor_overrides.enable_language_server,
+            data.editor.enable_language_server,
         );
-        merge_option(&mut self.language_settings.soft_wrap, data.editor.soft_wrap);
-        merge_option(&mut self.language_settings.tab_size, data.editor.tab_size);
+        merge_option(&mut self.editor_overrides.soft_wrap, data.editor.soft_wrap);
+        merge_option(&mut self.editor_overrides.tab_size, data.editor.tab_size);
         merge_option(
-            &mut self.language_settings.preferred_line_length,
+            &mut self.editor_overrides.preferred_line_length,
             data.editor.preferred_line_length,
         );
 
-        for (language_name, settings) in data.language_overrides.clone().into_iter() {
+        for (language_name, settings) in data.languages.clone().into_iter() {
             let target = self
                 .language_overrides
                 .entry(language_name.into())
@@ -270,77 +287,61 @@ pub fn settings_file_json_schema(
     let generator = SchemaGenerator::new(settings);
     let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
 
-    // Construct theme names reference type
-    let theme_names = theme_names
-        .into_iter()
-        .map(|name| Value::String(name))
-        .collect();
-    let theme_names_schema = Schema::Object(SchemaObject {
+    // Create a schema for a theme name.
+    let theme_name_schema = SchemaObject {
         instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
-        enum_values: Some(theme_names),
+        enum_values: Some(
+            theme_names
+                .into_iter()
+                .map(|name| Value::String(name))
+                .collect(),
+        ),
         ..Default::default()
-    });
-    root_schema
-        .definitions
-        .insert("ThemeName".to_owned(), theme_names_schema);
+    };
 
-    // Construct language settings reference type
-    let language_settings_schema_reference = Schema::Object(SchemaObject {
-        reference: Some("#/definitions/LanguageSettings".to_owned()),
-        ..Default::default()
-    });
-    let language_settings_properties = language_names
-        .into_iter()
-        .map(|name| {
-            (
-                name,
-                Schema::Object(SchemaObject {
-                    subschemas: Some(Box::new(SubschemaValidation {
-                        all_of: Some(vec![language_settings_schema_reference.clone()]),
-                        ..Default::default()
-                    })),
-                    ..Default::default()
-                }),
-            )
-        })
-        .collect();
-    let language_overrides_schema = Schema::Object(SchemaObject {
+    // Create a schema for a 'languages overrides' object, associating editor
+    // settings with specific langauges.
+    assert!(root_schema.definitions.contains_key("EditorSettings"));
+    let languages_object_schema = SchemaObject {
         instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
         object: Some(Box::new(ObjectValidation {
-            properties: language_settings_properties,
+            properties: language_names
+                .into_iter()
+                .map(|name| (name, Schema::new_ref("#/definitions/EditorSettings".into())))
+                .collect(),
             ..Default::default()
         })),
         ..Default::default()
-    });
+    };
+
+    // Add these new schemas as definitions, and modify properties of the root
+    // schema to reference them.
+    root_schema.definitions.extend([
+        ("ThemeName".into(), theme_name_schema.into()),
+        ("Languages".into(), languages_object_schema.into()),
+    ]);
     root_schema
-        .definitions
-        .insert("LanguageOverrides".to_owned(), language_overrides_schema);
+        .schema
+        .object
+        .as_mut()
+        .unwrap()
+        .properties
+        .extend([
+            (
+                "theme".to_owned(),
+                Schema::new_ref("#/definitions/ThemeName".into()),
+            ),
+            (
+                "languages".to_owned(),
+                Schema::new_ref("#/definitions/Languages".into()),
+            ),
+            // For backward compatibility
+            (
+                "language_overrides".to_owned(),
+                Schema::new_ref("#/definitions/Languages".into()),
+            ),
+        ]);
 
-    // Modify theme property to use new theme reference type
-    let settings_file_schema = root_schema.schema.object.as_mut().unwrap();
-    let language_overrides_schema_reference = Schema::Object(SchemaObject {
-        reference: Some("#/definitions/ThemeName".to_owned()),
-        ..Default::default()
-    });
-    settings_file_schema.properties.insert(
-        "theme".to_owned(),
-        Schema::Object(SchemaObject {
-            subschemas: Some(Box::new(SubschemaValidation {
-                all_of: Some(vec![language_overrides_schema_reference]),
-                ..Default::default()
-            })),
-            ..Default::default()
-        }),
-    );
-
-    // Modify language_overrides property to use LanguageOverrides reference
-    settings_file_schema.properties.insert(
-        "language_overrides".to_owned(),
-        Schema::Object(SchemaObject {
-            reference: Some("#/definitions/LanguageOverrides".to_owned()),
-            ..Default::default()
-        }),
-    );
     serde_json::to_value(root_schema).unwrap()
 }
 

crates/theme/src/theme.rs 🔗

@@ -12,8 +12,6 @@ use std::{collections::HashMap, sync::Arc};
 
 pub use theme_registry::*;
 
-pub const DEFAULT_THEME_NAME: &'static str = "cave-dark";
-
 #[derive(Deserialize, Default)]
 pub struct Theme {
     #[serde(default)]

crates/zed/src/main.rs 🔗

@@ -38,7 +38,7 @@ use std::{
     time::Duration,
 };
 use terminal;
-use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
+use theme::ThemeRegistry;
 use util::{ResultExt, TryFutureExt};
 use workspace::{self, AppState, NewFile, OpenPaths};
 use zed::{
@@ -72,73 +72,7 @@ fn main() {
 
     let fs = Arc::new(RealFs);
     let themes = ThemeRegistry::new(Assets, app.font_cache());
-    let theme = themes.get(DEFAULT_THEME_NAME).unwrap();
-    let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme)
-        .unwrap()
-        .with_language_defaults(
-            languages::PLAIN_TEXT.name(),
-            settings::LanguageSettings {
-                soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
-                ..Default::default()
-            },
-        )
-        .with_language_defaults(
-            "C",
-            settings::LanguageSettings {
-                tab_size: Some(2.try_into().unwrap()),
-                ..Default::default()
-            },
-        )
-        .with_language_defaults(
-            "C++",
-            settings::LanguageSettings {
-                tab_size: Some(2.try_into().unwrap()),
-                ..Default::default()
-            },
-        )
-        .with_language_defaults(
-            "Go",
-            settings::LanguageSettings {
-                tab_size: Some(4.try_into().unwrap()),
-                hard_tabs: Some(true),
-                ..Default::default()
-            },
-        )
-        .with_language_defaults(
-            "Markdown",
-            settings::LanguageSettings {
-                soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
-                ..Default::default()
-            },
-        )
-        .with_language_defaults(
-            "Rust",
-            settings::LanguageSettings {
-                tab_size: Some(4.try_into().unwrap()),
-                ..Default::default()
-            },
-        )
-        .with_language_defaults(
-            "JavaScript",
-            settings::LanguageSettings {
-                tab_size: Some(2.try_into().unwrap()),
-                ..Default::default()
-            },
-        )
-        .with_language_defaults(
-            "TypeScript",
-            settings::LanguageSettings {
-                tab_size: Some(2.try_into().unwrap()),
-                ..Default::default()
-            },
-        )
-        .with_language_defaults(
-            "TSX",
-            settings::LanguageSettings {
-                tab_size: Some(2.try_into().unwrap()),
-                ..Default::default()
-            },
-        );
+    let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes);
 
     let config_files = load_config_files(&app, fs.clone());
 

crates/zed/src/menus.rs 🔗

@@ -26,6 +26,10 @@ pub fn menus() -> Vec<Menu<'static>> {
                             name: "Open Key Bindings",
                             action: Box::new(super::OpenKeymap),
                         },
+                        MenuItem::Action {
+                            name: "Open Default Settings",
+                            action: Box::new(super::OpenDefaultSettings),
+                        },
                         MenuItem::Action {
                             name: "Open Default Key Bindings",
                             action: Box::new(super::OpenDefaultKeymap),

crates/zed/src/settings_file.rs 🔗

@@ -93,7 +93,7 @@ pub async fn watch_keymap_file(
 mod tests {
     use super::*;
     use project::FakeFs;
-    use settings::{LanguageSettings, SoftWrap};
+    use settings::{EditorSettings, SoftWrap};
 
     #[gpui::test]
     async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
@@ -128,7 +128,7 @@ mod tests {
 
         let settings = cx.read(Settings::test).with_language_defaults(
             "JavaScript",
-            LanguageSettings {
+            EditorSettings {
                 tab_size: Some(2.try_into().unwrap()),
                 ..Default::default()
             },

crates/zed/src/zed.rs 🔗

@@ -18,8 +18,9 @@ use gpui::{
     geometry::vector::vec2f,
     impl_actions,
     platform::{WindowBounds, WindowOptions},
-    AsyncAppContext, ViewContext,
+    AssetSource, AsyncAppContext, ViewContext,
 };
+use language::Rope;
 use lazy_static::lazy_static;
 pub use lsp;
 pub use project::{self, fs};
@@ -52,6 +53,7 @@ actions!(
         DebugElements,
         OpenSettings,
         OpenKeymap,
+        OpenDefaultSettings,
         OpenDefaultKeymap,
         IncreaseBufferFontSize,
         DecreaseBufferFontSize,
@@ -99,39 +101,48 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     cx.add_action({
         let app_state = app_state.clone();
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
-            open_config_file(&SETTINGS_PATH, app_state.clone(), cx);
+            open_config_file(&SETTINGS_PATH, app_state.clone(), cx, || {
+                let header = Assets.load("settings/header-comments.json").unwrap();
+                let json = Assets.load("settings/default.json").unwrap();
+                let header = str::from_utf8(header.as_ref()).unwrap();
+                let json = str::from_utf8(json.as_ref()).unwrap();
+                let mut content = Rope::new();
+                content.push(header);
+                content.push(json);
+                content
+            });
         }
     });
     cx.add_action({
         let app_state = app_state.clone();
         move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
-            open_config_file(&KEYMAP_PATH, app_state.clone(), cx);
+            open_config_file(&KEYMAP_PATH, app_state.clone(), cx, || Default::default());
         }
     });
     cx.add_action({
         let app_state = app_state.clone();
         move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
-            workspace.with_local_workspace(cx, app_state.clone(), |workspace, cx| {
-                let project = workspace.project().clone();
-                let buffer = project.update(cx, |project, cx| {
-                    let text = Assets::get("keymaps/default.json").unwrap().data;
-                    let text = str::from_utf8(text.as_ref()).unwrap();
-                    project
-                        .create_buffer(text, project.languages().get_language("JSON"), cx)
-                        .expect("creating buffers on a local workspace always succeeds")
-                });
-                let buffer = cx.add_model(|cx| {
-                    MultiBuffer::singleton(buffer, cx).with_title("Default Key Bindings".into())
-                });
-                workspace.add_item(
-                    Box::new(
-                        cx.add_view(|cx| {
-                            Editor::for_multibuffer(buffer, Some(project.clone()), cx)
-                        }),
-                    ),
-                    cx,
-                );
-            });
+            open_bundled_config_file(
+                workspace,
+                app_state.clone(),
+                "keymaps/default.json",
+                "Default Key Bindings",
+                cx,
+            );
+        }
+    });
+    cx.add_action({
+        let app_state = app_state.clone();
+        move |workspace: &mut Workspace,
+              _: &OpenDefaultSettings,
+              cx: &mut ViewContext<Workspace>| {
+            open_bundled_config_file(
+                workspace,
+                app_state.clone(),
+                "settings/default.json",
+                "Default Settings",
+                cx,
+            );
         }
     });
     cx.add_action(
@@ -366,12 +377,15 @@ fn open_config_file(
     path: &'static Path,
     app_state: Arc<AppState>,
     cx: &mut ViewContext<Workspace>,
+    default_content: impl 'static + Send + FnOnce() -> Rope,
 ) {
     cx.spawn(|workspace, mut cx| async move {
         let fs = &app_state.fs;
         if !fs.is_file(path).await {
             fs.create_dir(&ROOT_PATH).await?;
             fs.create_file(path, Default::default()).await?;
+            fs.save(path, &default_content(), Default::default())
+                .await?;
         }
 
         workspace
@@ -386,6 +400,30 @@ fn open_config_file(
     .detach_and_log_err(cx)
 }
 
+fn open_bundled_config_file(
+    workspace: &mut Workspace,
+    app_state: Arc<AppState>,
+    asset_path: &'static str,
+    title: &str,
+    cx: &mut ViewContext<Workspace>,
+) {
+    workspace.with_local_workspace(cx, app_state.clone(), |workspace, cx| {
+        let project = workspace.project().clone();
+        let buffer = project.update(cx, |project, cx| {
+            let text = Assets::get(asset_path).unwrap().data;
+            let text = str::from_utf8(text.as_ref()).unwrap();
+            project
+                .create_buffer(text, project.languages().get_language("JSON"), cx)
+                .expect("creating buffers on a local workspace always succeeds")
+        });
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
+        workspace.add_item(
+            Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), cx))),
+            cx,
+        );
+    });
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -400,7 +438,7 @@ mod tests {
         collections::HashSet,
         path::{Path, PathBuf},
     };
-    use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
+    use theme::ThemeRegistry;
     use workspace::{
         open_paths, pane, Item, ItemHandle, NewFile, Pane, SplitDirection, WorkspaceHandle,
     };
@@ -1530,23 +1568,29 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_bundled_themes(cx: &mut MutableAppContext) {
+    fn test_bundled_settings_and_themes(cx: &mut MutableAppContext) {
+        cx.platform()
+            .fonts()
+            .add_fonts(&[
+                Assets
+                    .load("fonts/zed-sans/zed-sans-extended.ttf")
+                    .unwrap()
+                    .to_vec()
+                    .into(),
+                Assets
+                    .load("fonts/zed-mono/zed-mono-extended.ttf")
+                    .unwrap()
+                    .to_vec()
+                    .into(),
+            ])
+            .unwrap();
         let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
-
-        lazy_static::lazy_static! {
-            static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
-            static ref FONTS: Vec<Arc<Vec<u8>>> = vec![
-                Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap().to_vec().into(),
-                Assets.load("fonts/zed-mono/zed-mono-extended.ttf").unwrap().to_vec().into(),
-            ];
-        }
-
-        cx.platform().fonts().add_fonts(&FONTS).unwrap();
+        let settings = Settings::defaults(Assets, cx.font_cache(), &themes);
 
         let mut has_default_theme = false;
         for theme_name in themes.list() {
             let theme = themes.get(&theme_name).unwrap();
-            if theme.name == DEFAULT_THEME_NAME {
+            if theme.name == settings.theme.name {
                 has_default_theme = true;
             }
             assert_eq!(theme.name, theme_name);