use futures::{stream, StreamExt}; use gpui::{executor, AsyncAppContext, FontCache}; use postage::sink::Sink as _; use postage::{prelude::Stream, watch}; use project::Fs; use serde::Deserialize; use settings::{KeymapFile, Settings, SettingsFileContent}; use std::{path::Path, sync::Arc, time::Duration}; use theme::ThemeRegistry; use util::ResultExt; #[derive(Clone)] pub struct WatchedJsonFile(watch::Receiver); impl WatchedJsonFile where T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync, { 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, rx) = watch::channel_with(settings); 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(T::default()) } } } pub fn settings_from_files( defaults: Settings, sources: Vec>, theme_registry: Arc, font_cache: Arc, ) -> impl futures::stream::Stream { stream::select_all(sources.iter().enumerate().map(|(i, source)| { let mut rx = source.0.clone(); // Consume the initial item from all of the constituent file watches but one. // This way, the stream will yield exactly one item for the files' initial // state, and won't return any more items until the files change. if i > 0 { rx.try_recv().ok(); } rx })) .map(move |_| { let mut settings = defaults.clone(); for source in &sources { settings.merge(&*source.0.borrow(), &theme_registry, &font_cache); } settings }) } pub async fn watch_keymap_file(mut file: WatchedJsonFile, mut cx: AsyncAppContext) { while let Some(content) = file.0.recv().await { cx.update(|cx| { cx.clear_bindings(); settings::KeymapFile::load_defaults(cx); content.add(cx).log_err(); }); } } #[cfg(test)] mod tests { use super::*; use project::FakeFs; use settings::SoftWrap; #[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 = WatchedJsonFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await; let source2 = WatchedJsonFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await; let source3 = WatchedJsonFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await; let mut settings_rx = settings_from_files( cx.read(Settings::test), vec![source1, source2, source3], ThemeRegistry::new((), cx.font_cache()), cx.font_cache(), ); let settings = settings_rx.next().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.next().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.next().await.unwrap(); assert_eq!(settings.tab_size, 4); } }