Add the ability for extensions to provide language settings (#10296)

Marshall Bowers , Max , and Max Brunsfeld created

This PR adds the ability for extensions to provide certain language
settings via the language `config.toml`.

These settings are then merged in with the rest of the settings when the
language is loaded from the extension.

The language settings that are available are:

- `tab_size`
- `hard_tabs`
- `soft_wrap`

Additionally, for bundled languages we moved these settings out of the
`settings/default.json` and into their respective `config.toml`s .

For languages currently provided by extensions, we are leaving the
values in the `settings/default.json` temporarily until all released
versions of Zed are able to load these settings from the extension.

---

Along the way we ended up refactoring the `Settings::load` method
slightly, introducing a new `SettingsSources` struct to better convey
where the settings are being loaded from.

This makes it easier to load settings from specific locations/sets of
locations in an explicit way.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

assets/settings/default.json                           |  36 -
crates/assistant/src/assistant_settings.rs             |   7 
crates/auto_update/src/auto_update.rs                  |  13 
crates/call/src/call_settings.rs                       |  13 
crates/client/src/client.rs                            |  28 
crates/collab_ui/src/panel_settings.rs                 |  30 
crates/diagnostics/src/project_diagnostics_settings.rs |  16 
crates/editor/src/editor_settings.rs                   |  10 
crates/extension/src/extension_settings.rs             |  13 
crates/journal/src/journal.rs                          |  10 
crates/language/src/language.rs                        |  18 
crates/language/src/language_registry.rs               |  17 
crates/language/src/language_settings.rs               |  26 
crates/languages/src/deno.rs                           |  14 
crates/languages/src/elixir.rs                         |  15 
crates/languages/src/elixir/config.toml                |   1 
crates/languages/src/go/config.toml                    |   2 
crates/languages/src/javascript/config.toml            |   1 
crates/languages/src/json/config.toml                  |   1 
crates/languages/src/lib.rs                            |  28 +
crates/languages/src/markdown/config.toml              |   3 
crates/languages/src/ocaml-interface/config.toml       |   1 
crates/languages/src/ocaml/config.toml                 |   1 
crates/languages/src/terraform/config.toml             |   1 
crates/languages/src/tsx/config.toml                   |   1 
crates/languages/src/typescript/config.toml            |   1 
crates/languages/src/yaml/config.toml                  |   1 
crates/project/src/project_settings.rs                 |   7 
crates/project_panel/src/project_panel_settings.rs     |   7 
crates/settings/src/settings.rs                        |   4 
crates/settings/src/settings_store.rs                  | 241 +++++++----
crates/tasks_ui/src/settings.rs                        |  16 
crates/terminal/src/terminal_settings.rs               |   8 
crates/theme/src/settings.rs                           |  17 
crates/vim/src/vim.rs                                  |  20 
crates/welcome/src/base_keymap_setting.rs              |  18 
crates/workspace/src/item.rs                           |  10 
crates/workspace/src/workspace_settings.rs             |  20 
crates/worktree/src/worktree_settings.rs               |   7 
crates/zed/src/zed.rs                                  |   2 
extensions/gleam/languages/gleam/config.toml           |   1 
extensions/prisma/languages/prisma/config.toml         |   1 
42 files changed, 349 insertions(+), 338 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -556,18 +556,10 @@
     "C": {
       "format_on_save": "off"
     },
-    "Plain Text": {
-      "soft_wrap": "preferred_line_length"
-    },
-    "Elixir": {
-      "tab_size": 2
-    },
     "Gleam": {
       "tab_size": 2
     },
     "Go": {
-      "tab_size": 4,
-      "hard_tabs": true,
       "code_actions_on_format": {
         "source.organizeImports": true
       }
@@ -575,34 +567,6 @@
     "Make": {
       "hard_tabs": true
     },
-    "Markdown": {
-      "tab_size": 2,
-      "soft_wrap": "preferred_line_length"
-    },
-    "JavaScript": {
-      "tab_size": 2
-    },
-    "Terraform": {
-      "tab_size": 2
-    },
-    "TypeScript": {
-      "tab_size": 2
-    },
-    "TSX": {
-      "tab_size": 2
-    },
-    "YAML": {
-      "tab_size": 2
-    },
-    "JSON": {
-      "tab_size": 2
-    },
-    "OCaml": {
-      "tab_size": 2
-    },
-    "OCaml Interface": {
-      "tab_size": 2
-    },
     "Prisma": {
       "tab_size": 2
     }

crates/assistant/src/assistant_settings.rs 🔗

@@ -10,7 +10,7 @@ use serde::{
     de::{self, Visitor},
     Deserialize, Deserializer, Serialize, Serializer,
 };
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 
 #[derive(Clone, Debug, Default, PartialEq)]
 pub enum ZedDotDevModel {
@@ -332,13 +332,12 @@ impl Settings for AssistantSettings {
     type FileContent = AssistantSettingsContent;
 
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
         let mut settings = AssistantSettings::default();
 
-        for value in [default_value].iter().chain(user_values) {
+        for value in sources.defaults_and_customizations() {
             let value = value.upgrade();
             merge(&mut settings.enabled, value.enabled);
             merge(&mut settings.button, value.button);

crates/auto_update/src/auto_update.rs 🔗

@@ -17,7 +17,7 @@ use serde::Deserialize;
 use serde_derive::Serialize;
 use smol::io::AsyncReadExt;
 
-use settings::{Settings, SettingsStore};
+use settings::{Settings, SettingsSources, SettingsStore};
 use smol::{fs::File, process::Command};
 
 use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
@@ -91,13 +91,12 @@ impl Settings for AutoUpdateSetting {
 
     type FileContent = AutoUpdateSettingOverride;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut AppContext,
-    ) -> Result<Self> {
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
         Ok(Self(
-            Self::json_merge(default_value, user_values)?
+            sources
+                .release_channel
+                .or(sources.user)
+                .unwrap_or(sources.default)
                 .0
                 .ok_or_else(Self::missing_default)?,
         ))

crates/call/src/call_settings.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::Result;
 use gpui::AppContext;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 
 #[derive(Deserialize, Debug)]
 pub struct CallSettings {
@@ -29,14 +29,7 @@ impl Settings for CallSettings {
 
     type FileContent = CallSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _cx: &mut AppContext,
-    ) -> Result<Self>
-    where
-        Self: Sized,
-    {
-        Self::load_via_json_merge(default_value, user_values)
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
     }
 }

crates/client/src/client.rs 🔗

@@ -28,7 +28,7 @@ use release_channel::{AppVersion, ReleaseChannel};
 use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
+use settings::{Settings, SettingsSources, SettingsStore};
 use std::fmt;
 use std::{
     any::TypeId,
@@ -97,15 +97,11 @@ impl Settings for ClientSettings {
 
     type FileContent = ClientSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut AppContext,
-    ) -> Result<Self>
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self>
     where
         Self: Sized,
     {
-        let mut result = Self::load_via_json_merge(default_value, user_values)?;
+        let mut result = sources.json_merge::<Self>()?;
         if let Some(server_url) = &*ZED_SERVER_URL {
             result.server_url = server_url.clone()
         }
@@ -427,21 +423,19 @@ impl settings::Settings for TelemetrySettings {
 
     type FileContent = TelemetrySettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut AppContext,
-    ) -> Result<Self> {
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
         Ok(Self {
-            diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or(
-                default_value
+            diagnostics: sources.user.as_ref().and_then(|v| v.diagnostics).unwrap_or(
+                sources
+                    .default
                     .diagnostics
                     .ok_or_else(Self::missing_default)?,
             ),
-            metrics: user_values
-                .first()
+            metrics: sources
+                .user
+                .as_ref()
                 .and_then(|v| v.metrics)
-                .unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?),
+                .unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?),
         })
     }
 }

crates/collab_ui/src/panel_settings.rs 🔗

@@ -2,7 +2,7 @@ use anyhow;
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 use workspace::dock::DockPosition;
 
 #[derive(Deserialize, Debug)]
@@ -53,48 +53,52 @@ pub struct MessageEditorSettings {
 
 impl Settings for CollaborationPanelSettings {
     const KEY: Option<&'static str> = Some("collaboration_panel");
+
     type FileContent = PanelSettingsContent;
+
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+        sources.json_merge()
     }
 }
 
 impl Settings for ChatPanelSettings {
     const KEY: Option<&'static str> = Some("chat_panel");
+
     type FileContent = PanelSettingsContent;
+
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+        sources.json_merge()
     }
 }
 
 impl Settings for NotificationPanelSettings {
     const KEY: Option<&'static str> = Some("notification_panel");
+
     type FileContent = PanelSettingsContent;
+
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+        sources.json_merge()
     }
 }
 
 impl Settings for MessageEditorSettings {
     const KEY: Option<&'static str> = Some("message_editor");
+
     type FileContent = MessageEditorSettings;
+
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+        sources.json_merge()
     }
 }

crates/diagnostics/src/project_diagnostics_settings.rs 🔗

@@ -1,5 +1,8 @@
+use anyhow::Result;
+use gpui::AppContext;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsSources};
 
 #[derive(Deserialize, Debug)]
 pub struct ProjectDiagnosticsSettings {
@@ -15,18 +18,11 @@ pub struct ProjectDiagnosticsSettingsContent {
     include_warnings: Option<bool>,
 }
 
-impl settings::Settings for ProjectDiagnosticsSettings {
+impl Settings for ProjectDiagnosticsSettings {
     const KEY: Option<&'static str> = Some("diagnostics");
     type FileContent = ProjectDiagnosticsSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _cx: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self>
-    where
-        Self: Sized,
-    {
-        Self::load_via_json_merge(default_value, user_values)
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
     }
 }

crates/editor/src/editor_settings.rs 🔗

@@ -1,6 +1,7 @@
+use gpui::AppContext;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 
 #[derive(Deserialize, Clone)]
 pub struct EditorSettings {
@@ -224,10 +225,9 @@ impl Settings for EditorSettings {
     type FileContent = EditorSettingsContent;
 
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
+        sources: SettingsSources<Self::FileContent>,
+        _: &mut AppContext,
     ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+        sources.json_merge()
     }
 }

crates/extension/src/extension_settings.rs 🔗

@@ -3,7 +3,7 @@ use collections::HashMap;
 use gpui::AppContext;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 use std::sync::Arc;
 
 #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
@@ -26,14 +26,7 @@ impl Settings for ExtensionSettings {
 
     type FileContent = Self;
 
-    fn load(
-        _default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _cx: &mut AppContext,
-    ) -> Result<Self>
-    where
-        Self: Sized,
-    {
-        Ok(user_values.get(0).copied().cloned().unwrap_or_default())
+    fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
+        Ok(sources.user.cloned().unwrap_or_default())
     }
 }

crates/journal/src/journal.rs 🔗

@@ -5,7 +5,7 @@ use editor::Editor;
 use gpui::{actions, AppContext, ViewContext, WindowContext};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 use std::{
     fs::OpenOptions,
     path::{Path, PathBuf},
@@ -50,12 +50,8 @@ impl settings::Settings for JournalSettings {
 
     type FileContent = Self;
 
-    fn load(
-        defaults: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut AppContext,
-    ) -> Result<Self> {
-        Self::load_via_json_merge(defaults, user_values)
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
     }
 }
 

crates/language/src/language.rs 🔗

@@ -38,6 +38,7 @@ use schemars::{
 use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
 use serde_json::Value;
 use smol::future::FutureExt as _;
+use std::num::NonZeroU32;
 use std::{
     any::Any,
     cell::RefCell,
@@ -73,6 +74,8 @@ pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
 pub use text::LineEnding;
 pub use tree_sitter::{Parser, Tree};
 
+use crate::language_settings::SoftWrap;
+
 /// Initializes the `language` crate.
 ///
 /// This should be called before making use of items from the create.
@@ -99,6 +102,7 @@ lazy_static! {
     pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
         LanguageConfig {
             name: "Plain Text".into(),
+            soft_wrap: Some(SoftWrap::PreferredLineLength),
             ..Default::default()
         },
         None,
@@ -576,6 +580,17 @@ pub struct LanguageConfig {
     /// The names of any Prettier plugins that should be used for this language.
     #[serde(default)]
     pub prettier_plugins: Vec<Arc<str>>,
+
+    /// Whether to indent lines using tab characters, as opposed to multiple
+    /// spaces.
+    #[serde(default)]
+    pub hard_tabs: Option<bool>,
+    /// How many columns a tab should occupy.
+    #[serde(default)]
+    pub tab_size: Option<NonZeroU32>,
+    /// How to soft-wrap long lines of text.
+    #[serde(default)]
+    pub soft_wrap: Option<SoftWrap>,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
@@ -660,6 +675,9 @@ impl Default for LanguageConfig {
             prettier_parser_name: None,
             prettier_plugins: Default::default(),
             collapsed_placeholder: Default::default(),
+            hard_tabs: Default::default(),
+            tab_size: Default::default(),
+            soft_wrap: Default::default(),
         }
     }
 }

crates/language/src/language_registry.rs 🔗

@@ -1,3 +1,4 @@
+use crate::language_settings::{AllLanguageSettingsContent, LanguageSettingsContent};
 use crate::{
     language_settings::all_language_settings, task_context::ContextProvider, CachedLspAdapter,
     File, Language, LanguageConfig, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter,
@@ -38,6 +39,7 @@ pub struct LanguageRegistry {
 struct LanguageRegistryState {
     next_language_server_id: usize,
     languages: Vec<Arc<Language>>,
+    language_settings: AllLanguageSettingsContent,
     available_languages: Vec<AvailableLanguage>,
     grammars: HashMap<Arc<str>, AvailableGrammar>,
     lsp_adapters: HashMap<Arc<str>, Vec<Arc<CachedLspAdapter>>>,
@@ -145,6 +147,7 @@ impl LanguageRegistry {
                 languages: Vec::new(),
                 available_languages: Vec::new(),
                 grammars: Default::default(),
+                language_settings: Default::default(),
                 loading_languages: Default::default(),
                 lsp_adapters: Default::default(),
                 subscription: watch::channel(),
@@ -338,6 +341,10 @@ impl LanguageRegistry {
         *state.subscription.0.borrow_mut() = ();
     }
 
+    pub fn language_settings(&self) -> AllLanguageSettingsContent {
+        self.state.read().language_settings.clone()
+    }
+
     pub fn language_names(&self) -> Vec<String> {
         let state = self.state.read();
         let mut result = state
@@ -854,6 +861,16 @@ impl LanguageRegistryState {
         if let Some(theme) = self.theme.as_ref() {
             language.set_theme(theme.syntax());
         }
+        self.language_settings.languages.insert(
+            language.name(),
+            LanguageSettingsContent {
+                tab_size: language.config.tab_size,
+                hard_tabs: language.config.hard_tabs,
+                soft_wrap: language.config.soft_wrap,
+                ..Default::default()
+            }
+            .clone(),
+        );
         self.languages.push(language);
         self.version += 1;
         *self.subscription.0.borrow_mut() = ();

crates/language/src/language_settings.rs 🔗

@@ -10,7 +10,7 @@ use schemars::{
     JsonSchema,
 };
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsLocation};
+use settings::{Settings, SettingsLocation, SettingsSources};
 use std::{num::NonZeroU32, path::Path, sync::Arc};
 
 impl<'a> Into<SettingsLocation<'a>> for &'a dyn File {
@@ -119,7 +119,7 @@ pub struct CopilotSettings {
 }
 
 /// The settings for all languages.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
 pub struct AllLanguageSettingsContent {
     /// The settings for enabling/disabling features.
     #[serde(default)]
@@ -140,7 +140,7 @@ pub struct AllLanguageSettingsContent {
 }
 
 /// The settings for a particular language.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
 pub struct LanguageSettingsContent {
     /// How many columns a tab should occupy.
     ///
@@ -249,7 +249,7 @@ pub struct LanguageSettingsContent {
 }
 
 /// The contents of the GitHub Copilot settings.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
 pub struct CopilotSettingsContent {
     /// A list of globs representing files that Copilot should be disabled for.
     #[serde(default)]
@@ -257,7 +257,7 @@ pub struct CopilotSettingsContent {
 }
 
 /// The settings for enabling/disabling features.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub struct FeaturesContent {
     /// Whether the GitHub Copilot feature is enabled.
@@ -473,11 +473,9 @@ impl settings::Settings for AllLanguageSettings {
 
     type FileContent = AllLanguageSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_settings: &[&Self::FileContent],
-        _: &mut AppContext,
-    ) -> Result<Self> {
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        let default_value = sources.default;
+
         // A default is provided for all settings.
         let mut defaults: LanguageSettings =
             serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
@@ -500,7 +498,8 @@ impl settings::Settings for AllLanguageSettings {
             .and_then(|c| c.disabled_globs.as_ref())
             .ok_or_else(Self::missing_default)?;
 
-        for user_settings in user_settings {
+        let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
+        for user_settings in sources.customizations() {
             if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
                 copilot_enabled = copilot;
             }
@@ -528,11 +527,8 @@ impl settings::Settings for AllLanguageSettings {
                     user_language_settings,
                 );
             }
-        }
 
-        let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
-        for user_file_types in user_settings.iter().map(|s| &s.file_types) {
-            for (language, suffixes) in user_file_types {
+            for (language, suffixes) in &user_settings.file_types {
                 file_types
                     .entry(language.clone())
                     .or_default()

crates/languages/src/deno.rs 🔗

@@ -2,12 +2,13 @@ use anyhow::{anyhow, bail, Context, Result};
 use async_trait::async_trait;
 use collections::HashMap;
 use futures::StreamExt;
+use gpui::AppContext;
 use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::{CodeActionKind, LanguageServerBinary};
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
 use serde_json::json;
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 use smol::{fs, fs::File};
 use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
 use util::{
@@ -31,15 +32,8 @@ impl Settings for DenoSettings {
 
     type FileContent = DenoSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
-    ) -> Result<Self>
-    where
-        Self: Sized,
-    {
-        Self::load_via_json_merge(default_value, user_values)
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
     }
 }
 

crates/languages/src/elixir.rs 🔗

@@ -1,14 +1,14 @@
 use anyhow::{anyhow, bail, Context, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use gpui::{AsyncAppContext, Task};
+use gpui::{AppContext, AsyncAppContext, Task};
 pub use language::*;
 use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
 use project::project_settings::ProjectSettings;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
 use serde_json::Value;
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 use smol::fs::{self, File};
 use std::{
     any::Any,
@@ -56,15 +56,8 @@ impl Settings for ElixirSettings {
 
     type FileContent = ElixirSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
-    ) -> Result<Self>
-    where
-        Self: Sized,
-    {
-        Self::load_via_json_merge(default_value, user_values)
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
     }
 }
 

crates/languages/src/elixir/config.toml 🔗

@@ -10,6 +10,7 @@ brackets = [
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
     { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+tab_size = 2
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
 
 [overrides.string]

crates/languages/src/go/config.toml 🔗

@@ -11,3 +11,5 @@ brackets = [
     { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
 ]
+tab_size = 4
+hard_tabs = true

crates/languages/src/javascript/config.toml 🔗

@@ -15,6 +15,7 @@ brackets = [
     { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
 ]
 word_characters = ["$", "#"]
+tab_size = 2
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
 prettier_parser_name = "babel"
 

crates/languages/src/lib.rs 🔗

@@ -1,11 +1,12 @@
 use anyhow::Context;
-use gpui::AppContext;
+use gpui::{AppContext, BorrowAppContext};
 pub use language::*;
 use node_runtime::NodeRuntime;
 use rust_embed::RustEmbed;
-use settings::Settings;
+use settings::{Settings, SettingsStore};
+use smol::stream::StreamExt;
 use std::{str, sync::Arc};
-use util::asset_str;
+use util::{asset_str, ResultExt};
 
 use crate::{elixir::elixir_task_context, rust::RustContextProvider};
 
@@ -327,6 +328,27 @@ pub fn init(
         "Svelte".into(),
         Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
     );
+
+    let mut subscription = languages.subscribe();
+    let mut prev_language_settings = languages.language_settings();
+
+    cx.spawn(|cx| async move {
+        while subscription.next().await.is_some() {
+            let language_settings = languages.language_settings();
+            if language_settings != prev_language_settings {
+                cx.update(|cx| {
+                    cx.update_global(|settings: &mut SettingsStore, cx| {
+                        settings
+                            .set_extension_settings(language_settings.clone(), cx)
+                            .log_err();
+                    });
+                })?;
+                prev_language_settings = language_settings;
+            }
+        }
+        anyhow::Ok(())
+    })
+    .detach();
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/languages/src/markdown/config.toml 🔗

@@ -12,3 +12,6 @@ brackets = [
     { start = "`", end = "`", close = false, newline = false },
 ]
 prettier_parser_name = "markdown"
+
+tab_size = 2
+soft_wrap = "preferred_line_length"

crates/languages/src/ocaml/config.toml 🔗

@@ -11,3 +11,4 @@ brackets = [
   { start = "(", end = ")", close = true, newline = true },
   { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }
 ]
+tab_size = 2

crates/languages/src/terraform/config.toml 🔗

@@ -12,3 +12,4 @@ brackets = [
     { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
 ]
+tab_size = 2

crates/languages/src/tsx/config.toml 🔗

@@ -16,6 +16,7 @@ brackets = [
 word_characters = ["#", "$"]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
 prettier_parser_name = "typescript"
+tab_size = 2
 
 [overrides.element]
 line_comments = { remove = true }

crates/project/src/project_settings.rs 🔗

@@ -2,7 +2,7 @@ use collections::HashMap;
 use gpui::AppContext;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 use std::sync::Arc;
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
@@ -61,10 +61,9 @@ impl Settings for ProjectSettings {
     type FileContent = Self;
 
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut AppContext,
     ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+        sources.json_merge()
     }
 }

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -2,7 +2,7 @@ use anyhow;
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
@@ -62,10 +62,9 @@ impl Settings for ProjectPanelSettings {
     type FileContent = ProjectPanelSettingsContent;
 
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+        sources.json_merge()
     }
 }

crates/settings/src/settings.rs 🔗

@@ -8,7 +8,9 @@ use util::asset_str;
 
 pub use keymap_file::KeymapFile;
 pub use settings_file::*;
-pub use settings_store::{Settings, SettingsJsonSchemaParams, SettingsLocation, SettingsStore};
+pub use settings_store::{
+    Settings, SettingsJsonSchemaParams, SettingsLocation, SettingsSources, SettingsStore,
+};
 
 #[derive(RustEmbed)]
 #[folder = "../../assets"]

crates/settings/src/settings_store.rs 🔗

@@ -29,14 +29,7 @@ pub trait Settings: 'static + Send + Sync {
 
     /// The logic for combining together values from one or more JSON files into the
     /// final value for this setting.
-    ///
-    /// The user values are ordered from least specific (the global settings file)
-    /// to most specific (the innermost local settings file).
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        cx: &mut AppContext,
-    ) -> Result<Self>
+    fn load(sources: SettingsSources<Self::FileContent>, cx: &mut AppContext) -> Result<Self>
     where
         Self: Sized;
 
@@ -48,31 +41,6 @@ pub trait Settings: 'static + Send + Sync {
         generator.root_schema_for::<Self::FileContent>()
     }
 
-    fn json_merge(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-    ) -> Result<Self::FileContent> {
-        let mut merged = serde_json::Value::Null;
-        for value in [default_value].iter().chain(user_values) {
-            merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
-        }
-        Ok(serde_json::from_value(merged)?)
-    }
-
-    fn load_via_json_merge(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-    ) -> Result<Self>
-    where
-        Self: DeserializeOwned,
-    {
-        let mut merged = serde_json::Value::Null;
-        for value in [default_value].iter().chain(user_values) {
-            merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
-        }
-        Ok(serde_json::from_value(merged)?)
-    }
-
     fn missing_default() -> anyhow::Error {
         anyhow::anyhow!("missing default")
     }
@@ -119,6 +87,48 @@ pub trait Settings: 'static + Send + Sync {
     }
 }
 
+#[derive(Clone, Copy, Debug)]
+pub struct SettingsSources<'a, T> {
+    /// The default Zed settings.
+    pub default: &'a T,
+    /// Settings provided by extensions.
+    pub extensions: Option<&'a T>,
+    /// The user settings.
+    pub user: Option<&'a T>,
+    /// The user settings for the current release channel.
+    pub release_channel: Option<&'a T>,
+    /// The project settings, ordered from least specific to most specific.
+    pub project: &'a [&'a T],
+}
+
+impl<'a, T: Serialize> SettingsSources<'a, T> {
+    /// Returns an iterator over the default settings as well as all settings customizations.
+    pub fn defaults_and_customizations(&self) -> impl Iterator<Item = &T> {
+        [self.default].into_iter().chain(self.customizations())
+    }
+
+    /// Returns an iterator over all of the settings customizations.
+    pub fn customizations(&self) -> impl Iterator<Item = &T> {
+        self.extensions
+            .into_iter()
+            .chain(self.user)
+            .chain(self.release_channel)
+            .chain(self.project.iter().copied())
+    }
+
+    /// Returns the settings after performing a JSON merge of the customizations into the
+    /// default settings.
+    ///
+    /// More-specific customizations win out over the less-specific ones.
+    pub fn json_merge<O: DeserializeOwned>(&self) -> Result<O> {
+        let mut merged = serde_json::Value::Null;
+        for value in self.defaults_and_customizations() {
+            merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
+        }
+        Ok(serde_json::from_value(merged)?)
+    }
+}
+
 #[derive(Clone, Copy)]
 pub struct SettingsLocation<'a> {
     pub worktree_id: usize,
@@ -136,6 +146,7 @@ pub struct SettingsStore {
     setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
     raw_default_settings: serde_json::Value,
     raw_user_settings: serde_json::Value,
+    raw_extension_settings: serde_json::Value,
     raw_local_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
     tab_size_callback: Option<(
         TypeId,
@@ -151,6 +162,7 @@ impl Default for SettingsStore {
             setting_values: Default::default(),
             raw_default_settings: serde_json::json!({}),
             raw_user_settings: serde_json::json!({}),
+            raw_extension_settings: serde_json::json!({}),
             raw_local_settings: Default::default(),
             tab_size_callback: Default::default(),
         }
@@ -169,8 +181,7 @@ trait AnySettingValue: 'static + Send + Sync {
     fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting>;
     fn load_setting(
         &self,
-        default_value: &DeserializedSetting,
-        custom: &[DeserializedSetting],
+        sources: SettingsSources<DeserializedSetting>,
         cx: &mut AppContext,
     ) -> Result<Box<dyn Any>>;
     fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
@@ -204,29 +215,35 @@ impl SettingsStore {
             .deserialize_setting(&self.raw_default_settings)
             .log_err()
         {
-            let mut user_values_stack = Vec::new();
-
-            if let Some(user_settings) = setting_value
+            let user_value = setting_value
                 .deserialize_setting(&self.raw_user_settings)
-                .log_err()
-            {
-                user_values_stack = vec![user_settings];
-            }
+                .log_err();
 
+            let mut release_channel_value = None;
             if let Some(release_settings) = &self
                 .raw_user_settings
                 .get(release_channel::RELEASE_CHANNEL.dev_name())
             {
-                if let Some(release_settings) = setting_value
+                release_channel_value = setting_value
                     .deserialize_setting(release_settings)
-                    .log_err()
-                {
-                    user_values_stack.push(release_settings);
-                }
+                    .log_err();
             }
 
+            let extension_value = setting_value
+                .deserialize_setting(&self.raw_extension_settings)
+                .log_err();
+
             if let Some(setting) = setting_value
-                .load_setting(&default_settings, &user_values_stack, cx)
+                .load_setting(
+                    SettingsSources {
+                        default: &default_settings,
+                        release_channel: release_channel_value.as_ref(),
+                        extensions: extension_value.as_ref(),
+                        user: user_value.as_ref(),
+                        project: &[],
+                    },
+                    cx,
+                )
                 .context("A default setting must be added to the `default.json` file")
                 .log_err()
             {
@@ -425,6 +442,21 @@ impl SettingsStore {
         Ok(())
     }
 
+    pub fn set_extension_settings<T: Serialize>(
+        &mut self,
+        content: T,
+        cx: &mut AppContext,
+    ) -> Result<()> {
+        let settings: serde_json::Value = serde_json::to_value(content)?;
+        if settings.is_object() {
+            self.raw_extension_settings = settings;
+            self.recompute_values(None, cx)?;
+            Ok(())
+        } else {
+            Err(anyhow!("settings must be an object"))
+        }
+    }
+
     /// Add or remove a set of local settings via a JSON string.
     pub fn clear_local_settings(&mut self, root_id: usize, cx: &mut AppContext) -> Result<()> {
         self.raw_local_settings.retain(|k, _| k.0 != root_id);
@@ -551,22 +583,20 @@ impl SettingsStore {
         cx: &mut AppContext,
     ) -> Result<()> {
         // Reload the global and local values for every setting.
-        let mut user_settings_stack = Vec::<DeserializedSetting>::new();
+        let mut project_settings_stack = Vec::<DeserializedSetting>::new();
         let mut paths_stack = Vec::<Option<(usize, &Path)>>::new();
         for setting_value in self.setting_values.values_mut() {
             let default_settings = setting_value.deserialize_setting(&self.raw_default_settings)?;
 
-            user_settings_stack.clear();
-            paths_stack.clear();
+            let extension_settings = setting_value
+                .deserialize_setting(&self.raw_extension_settings)
+                .log_err();
 
-            if let Some(user_settings) = setting_value
+            let user_settings = setting_value
                 .deserialize_setting(&self.raw_user_settings)
-                .log_err()
-            {
-                user_settings_stack.push(user_settings);
-                paths_stack.push(None);
-            }
+                .log_err();
 
+            let mut release_channel_settings = None;
             if let Some(release_settings) = &self
                 .raw_user_settings
                 .get(release_channel::RELEASE_CHANNEL.dev_name())
@@ -575,15 +605,25 @@ impl SettingsStore {
                     .deserialize_setting(release_settings)
                     .log_err()
                 {
-                    user_settings_stack.push(release_settings);
-                    paths_stack.push(None);
+                    release_channel_settings = Some(release_settings);
                 }
             }
 
             // If the global settings file changed, reload the global value for the field.
+            project_settings_stack.clear();
+            paths_stack.clear();
             if changed_local_path.is_none() {
                 if let Some(value) = setting_value
-                    .load_setting(&default_settings, &user_settings_stack, cx)
+                    .load_setting(
+                        SettingsSources {
+                            default: &default_settings,
+                            extensions: extension_settings.as_ref(),
+                            user: user_settings.as_ref(),
+                            release_channel: release_channel_settings.as_ref(),
+                            project: &[],
+                        },
+                        cx,
+                    )
                     .log_err()
                 {
                     setting_value.set_global_value(value);
@@ -597,7 +637,7 @@ impl SettingsStore {
                     if let Some((prev_root_id, prev_path)) = prev_entry {
                         if root_id != prev_root_id || !path.starts_with(prev_path) {
                             paths_stack.pop();
-                            user_settings_stack.pop();
+                            project_settings_stack.pop();
                             continue;
                         }
                     }
@@ -608,7 +648,7 @@ impl SettingsStore {
                     setting_value.deserialize_setting(local_settings).log_err()
                 {
                     paths_stack.push(Some((*root_id, path.as_ref())));
-                    user_settings_stack.push(local_settings);
+                    project_settings_stack.push(local_settings);
 
                     // If a local settings file changed, then avoid recomputing local
                     // settings for any path outside of that directory.
@@ -619,7 +659,16 @@ impl SettingsStore {
                     }
 
                     if let Some(value) = setting_value
-                        .load_setting(&default_settings, &user_settings_stack, cx)
+                        .load_setting(
+                            SettingsSources {
+                                default: &default_settings,
+                                extensions: extension_settings.as_ref(),
+                                user: user_settings.as_ref(),
+                                release_channel: release_channel_settings.as_ref(),
+                                project: &project_settings_stack.iter().collect::<Vec<_>>(),
+                            },
+                            cx,
+                        )
                         .log_err()
                     {
                         setting_value.set_local_value(*root_id, path.clone(), value);
@@ -660,16 +709,30 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
 
     fn load_setting(
         &self,
-        default_value: &DeserializedSetting,
-        user_values: &[DeserializedSetting],
+        values: SettingsSources<DeserializedSetting>,
         cx: &mut AppContext,
     ) -> Result<Box<dyn Any>> {
-        let default_value = default_value.0.downcast_ref::<T::FileContent>().unwrap();
-        let values: SmallVec<[&T::FileContent; 6]> = user_values
-            .iter()
-            .map(|value| value.0.downcast_ref().unwrap())
-            .collect();
-        Ok(Box::new(T::load(default_value, &values, cx)?))
+        Ok(Box::new(T::load(
+            SettingsSources {
+                default: values.default.0.downcast_ref::<T::FileContent>().unwrap(),
+                extensions: values
+                    .extensions
+                    .map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
+                user: values
+                    .user
+                    .map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
+                release_channel: values
+                    .release_channel
+                    .map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
+                project: values
+                    .project
+                    .iter()
+                    .map(|value| value.0.downcast_ref().unwrap())
+                    .collect::<SmallVec<[_; 3]>>()
+                    .as_slice(),
+            },
+            cx,
+        )?))
     }
 
     fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result<DeserializedSetting> {
@@ -1277,12 +1340,8 @@ mod tests {
         const KEY: Option<&'static str> = Some("user");
         type FileContent = UserSettingsJson;
 
-        fn load(
-            default_value: &UserSettingsJson,
-            user_values: &[&UserSettingsJson],
-            _: &mut AppContext,
-        ) -> Result<Self> {
-            Self::load_via_json_merge(default_value, user_values)
+        fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+            sources.json_merge()
         }
     }
 
@@ -1293,12 +1352,8 @@ mod tests {
         const KEY: Option<&'static str> = Some("turbo");
         type FileContent = Option<bool>;
 
-        fn load(
-            default_value: &Option<bool>,
-            user_values: &[&Option<bool>],
-            _: &mut AppContext,
-        ) -> Result<Self> {
-            Self::load_via_json_merge(default_value, user_values)
+        fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+            sources.json_merge()
         }
     }
 
@@ -1321,12 +1376,8 @@ mod tests {
 
         type FileContent = MultiKeySettingsJson;
 
-        fn load(
-            default_value: &MultiKeySettingsJson,
-            user_values: &[&MultiKeySettingsJson],
-            _: &mut AppContext,
-        ) -> Result<Self> {
-            Self::load_via_json_merge(default_value, user_values)
+        fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+            sources.json_merge()
         }
     }
 
@@ -1354,12 +1405,8 @@ mod tests {
 
         type FileContent = JournalSettingsJson;
 
-        fn load(
-            default_value: &JournalSettingsJson,
-            user_values: &[&JournalSettingsJson],
-            _: &mut AppContext,
-        ) -> Result<Self> {
-            Self::load_via_json_merge(default_value, user_values)
+        fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+            sources.json_merge()
         }
     }
 
@@ -1380,8 +1427,8 @@ mod tests {
 
         type FileContent = Self;
 
-        fn load(default_value: &Self, user_values: &[&Self], _: &mut AppContext) -> Result<Self> {
-            Self::load_via_json_merge(default_value, user_values)
+        fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+            sources.json_merge()
         }
     }
 }

crates/tasks_ui/src/settings.rs 🔗

@@ -1,5 +1,6 @@
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsSources};
 
 #[derive(Serialize, Deserialize, PartialEq, Default)]
 pub(crate) struct TaskSettings {
@@ -13,22 +14,15 @@ pub(crate) struct TaskSettingsContent {
     show_status_indicator: Option<bool>,
 }
 
-impl settings::Settings for TaskSettings {
+impl Settings for TaskSettings {
     const KEY: Option<&'static str> = Some("task");
 
     type FileContent = TaskSettingsContent;
 
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut gpui::AppContext,
-    ) -> gpui::Result<Self>
-    where
-        Self: Sized,
-    {
-        let this = Self::json_merge(default_value, user_values)?;
-        Ok(Self {
-            show_status_indicator: this.show_status_indicator.unwrap_or(true),
-        })
+    ) -> gpui::Result<Self> {
+        sources.json_merge()
     }
 }

crates/terminal/src/terminal_settings.rs 🔗

@@ -7,7 +7,7 @@ use schemars::{
 };
 use serde_derive::{Deserialize, Serialize};
 use serde_json::Value;
-use settings::SettingsJsonSchemaParams;
+use settings::{SettingsJsonSchemaParams, SettingsSources};
 use std::path::PathBuf;
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -171,12 +171,12 @@ impl settings::Settings for TerminalSettings {
     type FileContent = TerminalSettingsContent;
 
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut AppContext,
     ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+        sources.json_merge()
     }
+
     fn json_schema(
         generator: &mut SchemaGenerator,
         params: &SettingsJsonSchemaParams,

crates/theme/src/settings.rs 🔗

@@ -14,7 +14,7 @@ use schemars::{
 };
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use settings::{Settings, SettingsJsonSchemaParams};
+use settings::{Settings, SettingsJsonSchemaParams, SettingsSources};
 use std::sync::Arc;
 use util::ResultExt as _;
 
@@ -316,14 +316,11 @@ impl settings::Settings for ThemeSettings {
 
     type FileContent = ThemeSettingsContent;
 
-    fn load(
-        defaults: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        cx: &mut AppContext,
-    ) -> Result<Self> {
+    fn load(sources: SettingsSources<Self::FileContent>, cx: &mut AppContext) -> Result<Self> {
         let themes = ThemeRegistry::default_global(cx);
         let system_appearance = SystemAppearance::default_global(cx);
 
+        let defaults = sources.default;
         let mut this = Self {
             ui_font_size: defaults.ui_font_size.unwrap().into(),
             ui_font: Font {
@@ -348,15 +345,15 @@ impl settings::Settings for ThemeSettings {
             theme_overrides: None,
         };
 
-        for value in user_values.iter().copied().cloned() {
-            if let Some(value) = value.buffer_font_family {
+        for value in sources.user.into_iter().chain(sources.release_channel) {
+            if let Some(value) = value.buffer_font_family.clone() {
                 this.buffer_font.family = value.into();
             }
             if let Some(value) = value.buffer_font_features {
                 this.buffer_font.features = value;
             }
 
-            if let Some(value) = value.ui_font_family {
+            if let Some(value) = value.ui_font_family.clone() {
                 this.ui_font.family = value.into();
             }
             if let Some(value) = value.ui_font_features {
@@ -373,7 +370,7 @@ impl settings::Settings for ThemeSettings {
                 }
             }
 
-            this.theme_overrides = value.theme_overrides;
+            this.theme_overrides = value.theme_overrides.clone();
             this.apply_theme_overrides();
 
             merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));

crates/vim/src/vim.rs 🔗

@@ -35,7 +35,7 @@ use replace::multi_replace;
 use schemars::JsonSchema;
 use serde::Deserialize;
 use serde_derive::Serialize;
-use settings::{update_settings_file, Settings, SettingsStore};
+use settings::{update_settings_file, Settings, SettingsSources, SettingsStore};
 use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
 use std::{ops::Range, sync::Arc};
 use surrounds::{add_surrounds, change_surrounds, delete_surrounds};
@@ -779,13 +779,9 @@ impl Settings for VimModeSetting {
 
     type FileContent = Option<bool>;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut AppContext,
-    ) -> Result<Self> {
-        Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
-            default_value.ok_or_else(Self::missing_default)?,
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        Ok(Self(sources.user.copied().flatten().unwrap_or(
+            sources.default.ok_or_else(Self::missing_default)?,
         )))
     }
 }
@@ -824,11 +820,7 @@ impl Settings for VimSettings {
 
     type FileContent = VimSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut AppContext,
-    ) -> Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
     }
 }

crates/welcome/src/base_keymap_setting.rs 🔗

@@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter};
 
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 
 /// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
 ///
@@ -70,16 +70,12 @@ impl Settings for BaseKeymap {
     type FileContent = Option<Self>;
 
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self>
-    where
-        Self: Sized,
-    {
-        Ok(user_values
-            .first()
-            .and_then(|v| **v)
-            .unwrap_or(default_value.unwrap()))
+    ) -> anyhow::Result<Self> {
+        if let Some(Some(user_value)) = sources.user.copied() {
+            return Ok(user_value);
+        }
+        sources.default.ok_or_else(Self::missing_default)
     }
 }

crates/workspace/src/item.rs 🔗

@@ -20,7 +20,7 @@ use gpui::{
 use project::{Project, ProjectEntryId, ProjectPath};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -76,12 +76,8 @@ impl Settings for ItemSettings {
 
     type FileContent = ItemSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut AppContext,
-    ) -> Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
     }
 }
 

crates/workspace/src/workspace_settings.rs 🔗

@@ -1,6 +1,8 @@
+use anyhow::Result;
+use gpui::AppContext;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 
 #[derive(Deserialize)]
 pub struct WorkspaceSettings {
@@ -63,12 +65,8 @@ impl Settings for WorkspaceSettings {
 
     type FileContent = WorkspaceSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
     }
 }
 
@@ -77,11 +75,7 @@ impl Settings for TabBarSettings {
 
     type FileContent = TabBarSettingsContent;
 
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
     }
 }

crates/worktree/src/worktree_settings.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::AppContext;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsSources};
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct WorktreeSettings {
@@ -31,10 +31,9 @@ impl Settings for WorktreeSettings {
     type FileContent = Self;
 
     fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
+        sources: SettingsSources<Self::FileContent>,
         _: &mut AppContext,
     ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
+        sources.json_merge()
     }
 }

crates/zed/src/zed.rs 🔗

@@ -607,6 +607,7 @@ pub fn handle_keymap_file_changes(
     cx.observe_global::<SettingsStore>(move |cx| {
         let new_base_keymap = *BaseKeymap::get_global(cx);
         let new_vim_enabled = VimModeSetting::get_global(cx).0;
+
         if new_base_keymap != old_base_keymap || new_vim_enabled != old_vim_enabled {
             old_base_keymap = new_base_keymap;
             old_vim_enabled = new_vim_enabled;
@@ -3062,6 +3063,7 @@ mod tests {
             let mut app_state = AppState::test(cx);
 
             let state = Arc::get_mut(&mut app_state).unwrap();
+            env_logger::try_init().ok();
 
             state.build_window_options = build_window_options;
             theme::init(theme::LoadThemes::JustBase, cx);

extensions/gleam/languages/gleam/config.toml 🔗

@@ -9,3 +9,4 @@ brackets = [
     { start = "(", end = ")", close = true, newline = true },
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+tab_size = 2