Start work on loading settings from a file

Max Brunsfeld and Keith Simmons created

Co-Authored-By: Keith Simmons <keith@zed.dev>

Change summary

Cargo.lock                       |   1 
crates/workspace/Cargo.toml      |   3 
crates/workspace/src/settings.rs | 274 +++++++++++++++++++++++++++++++--
crates/zed/src/main.rs           |   4 
4 files changed, 259 insertions(+), 23 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5851,6 +5851,7 @@ dependencies = [
  "parking_lot",
  "postage",
  "project",
+ "serde",
  "serde_json",
  "theme",
  "util",

crates/workspace/Cargo.toml 🔗

@@ -24,9 +24,10 @@ futures = "0.3"
 log = "0.4"
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
+serde = { version = "1", features = ["derive", "rc"] }
+serde_json = { version = "1", features = ["preserve_order"] }
 
 [dev-dependencies]
 client = { path = "../client", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
-serde_json = { version = "1.0.64", features = ["preserve_order"] }

crates/workspace/src/settings.rs 🔗

@@ -1,34 +1,128 @@
 use anyhow::Result;
-use gpui::font_cache::{FamilyId, FontCache};
+use futures::{stream, SinkExt, StreamExt as _};
+use gpui::{
+    executor,
+    font_cache::{FamilyId, FontCache},
+};
 use language::Language;
-use std::{collections::HashMap, sync::Arc};
-use theme::Theme;
+use parking_lot::Mutex;
+use postage::{prelude::Stream, watch};
+use project::Fs;
+use serde::Deserialize;
+use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
+use theme::{Theme, ThemeRegistry};
+use util::ResultExt;
 
 #[derive(Clone)]
 pub struct Settings {
     pub buffer_font_family: FamilyId,
     pub buffer_font_size: f32,
     pub tab_size: usize,
-    soft_wrap: SoftWrap,
-    preferred_line_length: u32,
-    overrides: HashMap<Arc<str>, Override>,
+    pub soft_wrap: SoftWrap,
+    pub preferred_line_length: u32,
+    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
     pub theme: Arc<Theme>,
 }
 
-#[derive(Clone, Default)]
-pub struct Override {
+#[derive(Clone, Debug, Default, Deserialize)]
+pub struct LanguageOverride {
+    pub tab_size: Option<usize>,
     pub soft_wrap: Option<SoftWrap>,
     pub preferred_line_length: Option<u32>,
 }
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
 pub enum SoftWrap {
     None,
     EditorWidth,
     PreferredLineLength,
 }
 
+#[derive(Clone)]
+pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
+
+#[derive(Clone, Debug, Default, Deserialize)]
+struct SettingsFileContent {
+    #[serde(default)]
+    buffer_font_family: Option<String>,
+    #[serde(default)]
+    buffer_font_size: Option<f32>,
+    #[serde(flatten)]
+    editor: LanguageOverride,
+    #[serde(default)]
+    language_overrides: HashMap<Arc<str>, LanguageOverride>,
+    #[serde(default)]
+    theme: Option<String>,
+}
+
+impl SettingsFile {
+    pub async fn new(
+        fs: Arc<dyn Fs>,
+        executor: &executor::Background,
+        path: impl Into<Arc<Path>>,
+    ) -> Self {
+        let path = path.into();
+        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
+        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
+        let (mut tx, mut rx) = watch::channel_with(settings);
+        rx.recv().await;
+        executor
+            .spawn(async move {
+                while events.next().await.is_some() {
+                    if let Some(settings) = Self::load(fs.clone(), &path).await {
+                        if tx.send(settings).await.is_err() {
+                            break;
+                        }
+                    }
+                }
+            })
+            .detach();
+        Self(rx)
+    }
+
+    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
+        if fs.is_file(&path).await {
+            fs.load(&path)
+                .await
+                .log_err()
+                .and_then(|data| serde_json::from_str(&data).log_err())
+        } else {
+            Some(SettingsFileContent::default())
+        }
+    }
+}
+
 impl Settings {
+    pub fn from_files(
+        defaults: Self,
+        sources: Vec<SettingsFile>,
+        executor: Arc<executor::Background>,
+        theme_registry: Arc<ThemeRegistry>,
+        font_cache: Arc<FontCache>,
+    ) -> (Arc<Mutex<watch::Sender<Self>>>, watch::Receiver<Self>) {
+        let (tx, mut rx) = watch::channel_with(defaults.clone());
+        let tx = Arc::new(Mutex::new(tx));
+        executor
+            .spawn({
+                let tx = tx.clone();
+                async move {
+                    let mut stream =
+                        stream::select_all(sources.iter().map(|source| source.0.clone()));
+                    while stream.next().await.is_some() {
+                        let mut settings = defaults.clone();
+                        for source in &sources {
+                            settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
+                        }
+                        *tx.lock().borrow_mut() = settings;
+                    }
+                }
+            })
+            .detach();
+        rx.try_recv().ok();
+        (tx, rx)
+    }
+
     pub fn new(
         buffer_font_family: &str,
         font_cache: &FontCache,
@@ -40,35 +134,38 @@ impl Settings {
             tab_size: 4,
             soft_wrap: SoftWrap::None,
             preferred_line_length: 80,
-            overrides: Default::default(),
+            language_overrides: Default::default(),
             theme,
         })
     }
 
-    pub fn with_tab_size(mut self, tab_size: usize) -> Self {
-        self.tab_size = tab_size;
-        self
-    }
-
     pub fn with_overrides(
         mut self,
         language_name: impl Into<Arc<str>>,
-        overrides: Override,
+        overrides: LanguageOverride,
     ) -> Self {
-        self.overrides.insert(language_name.into(), overrides);
+        self.language_overrides
+            .insert(language_name.into(), overrides);
         self
     }
 
+    pub fn tab_size(&self, language: Option<&Arc<Language>>) -> usize {
+        language
+            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
+            .and_then(|settings| settings.tab_size)
+            .unwrap_or(self.tab_size)
+    }
+
     pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
         language
-            .and_then(|language| self.overrides.get(language.name().as_ref()))
+            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
             .and_then(|settings| settings.soft_wrap)
             .unwrap_or(self.soft_wrap)
     }
 
     pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
         language
-            .and_then(|language| self.overrides.get(language.name().as_ref()))
+            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
             .and_then(|settings| settings.preferred_line_length)
             .unwrap_or(self.preferred_line_length)
     }
@@ -81,8 +178,145 @@ impl Settings {
             tab_size: 4,
             soft_wrap: SoftWrap::None,
             preferred_line_length: 80,
-            overrides: Default::default(),
+            language_overrides: Default::default(),
             theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
         }
     }
+
+    fn merge(
+        &mut self,
+        data: &SettingsFileContent,
+        theme_registry: &ThemeRegistry,
+        font_cache: &FontCache,
+    ) {
+        if let Some(value) = &data.buffer_font_family {
+            if let Some(id) = font_cache.load_family(&[value]).log_err() {
+                self.buffer_font_family = id;
+            }
+        }
+        if let Some(value) = &data.theme {
+            if let Some(theme) = theme_registry.get(value).log_err() {
+                self.theme = theme;
+            }
+        }
+
+        merge(&mut self.buffer_font_size, data.buffer_font_size);
+        merge(&mut self.soft_wrap, data.editor.soft_wrap);
+        merge(&mut self.tab_size, data.editor.tab_size);
+        merge(
+            &mut self.preferred_line_length,
+            data.editor.preferred_line_length,
+        );
+
+        for (language_name, settings) in &data.language_overrides {
+            let target = self
+                .language_overrides
+                .entry(language_name.clone())
+                .or_default();
+
+            merge_option(&mut target.tab_size, settings.tab_size);
+            merge_option(&mut target.soft_wrap, settings.soft_wrap);
+            merge_option(
+                &mut target.preferred_line_length,
+                settings.preferred_line_length,
+            );
+        }
+    }
+}
+
+fn merge<T: Copy>(target: &mut T, value: Option<T>) {
+    if let Some(value) = value {
+        *target = value;
+    }
+}
+
+fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
+    if value.is_some() {
+        *target = value;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use postage::prelude::Stream;
+    use project::FakeFs;
+
+    #[gpui::test]
+    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
+        let executor = cx.background();
+        let fs = FakeFs::new(executor.clone());
+
+        fs.save(
+            "/settings1.json".as_ref(),
+            &r#"
+            {
+                "buffer_font_size": 24,
+                "soft_wrap": "editor_width",
+                "language_overrides": {
+                    "Markdown": {
+                        "preferred_line_length": 100,
+                        "soft_wrap": "preferred_line_length"
+                    }
+                }
+            }
+            "#
+            .into(),
+        )
+        .await
+        .unwrap();
+
+        let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
+        let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
+        let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
+
+        let (_, mut settings_rx) = Settings::from_files(
+            cx.read(Settings::test),
+            vec![source1, source2, source3],
+            cx.background(),
+            ThemeRegistry::new((), cx.font_cache()),
+            cx.font_cache(),
+        );
+
+        let settings = settings_rx.recv().await.unwrap();
+        let md_settings = settings.language_overrides.get("Markdown").unwrap();
+        assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
+        assert_eq!(settings.buffer_font_size, 24.0);
+        assert_eq!(settings.tab_size, 4);
+        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+        assert_eq!(md_settings.preferred_line_length, Some(100));
+
+        fs.save(
+            "/settings2.json".as_ref(),
+            &r#"
+            {
+                "tab_size": 2,
+                "soft_wrap": "none",
+                "language_overrides": {
+                    "Markdown": {
+                        "preferred_line_length": 120
+                    }
+                }
+            }
+            "#
+            .into(),
+        )
+        .await
+        .unwrap();
+
+        let settings = settings_rx.recv().await.unwrap();
+        let md_settings = settings.language_overrides.get("Markdown").unwrap();
+        assert_eq!(settings.soft_wrap, SoftWrap::None);
+        assert_eq!(settings.buffer_font_size, 24.0);
+        assert_eq!(settings.tab_size, 2);
+        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+        assert_eq!(md_settings.preferred_line_length, Some(120));
+
+        fs.remove_file("/settings2.json".as_ref(), Default::default())
+            .await
+            .unwrap();
+
+        let settings = settings_rx.recv().await.unwrap();
+        assert_eq!(settings.tab_size, 4);
+    }
 }

crates/zed/src/main.rs 🔗

@@ -29,14 +29,14 @@ fn main() {
         .unwrap()
         .with_overrides(
             language::PLAIN_TEXT.name(),
-            settings::Override {
+            settings::LanguageOverride {
                 soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
                 ..Default::default()
             },
         )
         .with_overrides(
             "Markdown",
-            settings::Override {
+            settings::LanguageOverride {
                 soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
                 ..Default::default()
             },