From 83f98dde52035aec9f2b497ec1fc9e3c2b78217a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Mar 2022 16:13:16 -0800 Subject: [PATCH] Start work on loading settings from a file Co-Authored-By: Keith Simmons --- 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(-) diff --git a/Cargo.lock b/Cargo.lock index 7aafce3bbb00774249f6ae29e8bbeed1fda559bb..2a50f76502942b8af6cc539bba0bc0b61a2cbcc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5851,6 +5851,7 @@ dependencies = [ "parking_lot", "postage", "project", + "serde", "serde_json", "theme", "util", diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index d83cbf29d4f8f64c8666e19323b8fd305bc1cae8..b987856450d6475119cc533671a68a30fe3ebaa0 100644 --- a/crates/workspace/Cargo.toml +++ b/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"] } diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs index 242ecbbce2a765670e02df12aec166dcbdc1a5b8..62a6d4af8c910e832033245d3920aa8d12c24cf5 100644 --- a/crates/workspace/src/settings.rs +++ b/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, Override>, + pub soft_wrap: SoftWrap, + pub preferred_line_length: u32, + pub language_overrides: HashMap, LanguageOverride>, pub theme: Arc, } -#[derive(Clone, Default)] -pub struct Override { +#[derive(Clone, Debug, Default, Deserialize)] +pub struct LanguageOverride { + pub tab_size: Option, pub soft_wrap: Option, pub preferred_line_length: Option, } -#[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); + +#[derive(Clone, Debug, Default, Deserialize)] +struct SettingsFileContent { + #[serde(default)] + buffer_font_family: Option, + #[serde(default)] + buffer_font_size: Option, + #[serde(flatten)] + editor: LanguageOverride, + #[serde(default)] + language_overrides: HashMap, LanguageOverride>, + #[serde(default)] + theme: Option, +} + +impl SettingsFile { + pub async fn new( + fs: Arc, + executor: &executor::Background, + path: impl Into>, + ) -> 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, path: &Path) -> Option { + 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, + executor: Arc, + theme_registry: Arc, + font_cache: Arc, + ) -> (Arc>>, watch::Receiver) { + 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>, - 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>) -> 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>) -> 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>) -> 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(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } +} + +fn merge_option(target: &mut Option, value: Option) { + 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); + } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0a3ca281337d8136cef1cfe9330a1879c60124fa..f2635fa7feaf5194de1a6926c867de6709bf98cb 100644 --- a/crates/zed/src/main.rs +++ b/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() },