settings_file.rs

  1use futures::{stream, StreamExt};
  2use gpui::{executor, AsyncAppContext, FontCache};
  3use postage::sink::Sink as _;
  4use postage::{prelude::Stream, watch};
  5use project::Fs;
  6use serde::Deserialize;
  7use settings::{KeymapFile, Settings, SettingsFileContent};
  8use std::{path::Path, sync::Arc, time::Duration};
  9use theme::ThemeRegistry;
 10use util::ResultExt;
 11
 12#[derive(Clone)]
 13pub struct WatchedJsonFile<T>(watch::Receiver<T>);
 14
 15impl<T> WatchedJsonFile<T>
 16where
 17    T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync,
 18{
 19    pub async fn new(
 20        fs: Arc<dyn Fs>,
 21        executor: &executor::Background,
 22        path: impl Into<Arc<Path>>,
 23    ) -> Self {
 24        let path = path.into();
 25        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
 26        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
 27        let (mut tx, rx) = watch::channel_with(settings);
 28        executor
 29            .spawn(async move {
 30                while events.next().await.is_some() {
 31                    if let Some(settings) = Self::load(fs.clone(), &path).await {
 32                        if tx.send(settings).await.is_err() {
 33                            break;
 34                        }
 35                    }
 36                }
 37            })
 38            .detach();
 39        Self(rx)
 40    }
 41
 42    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
 43        if fs.is_file(&path).await {
 44            fs.load(&path)
 45                .await
 46                .log_err()
 47                .and_then(|data| serde_json::from_str(&data).log_err())
 48        } else {
 49            Some(T::default())
 50        }
 51    }
 52}
 53
 54pub fn settings_from_files(
 55    defaults: Settings,
 56    sources: Vec<WatchedJsonFile<SettingsFileContent>>,
 57    theme_registry: Arc<ThemeRegistry>,
 58    font_cache: Arc<FontCache>,
 59) -> impl futures::stream::Stream<Item = Settings> {
 60    stream::select_all(sources.iter().enumerate().map(|(i, source)| {
 61        let mut rx = source.0.clone();
 62        // Consume the initial item from all of the constituent file watches but one.
 63        // This way, the stream will yield exactly one item for the files' initial
 64        // state, and won't return any more items until the files change.
 65        if i > 0 {
 66            rx.try_recv().ok();
 67        }
 68        rx
 69    }))
 70    .map(move |_| {
 71        let mut settings = defaults.clone();
 72        for source in &sources {
 73            settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
 74        }
 75        settings
 76    })
 77}
 78
 79pub async fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFile>, mut cx: AsyncAppContext) {
 80    while let Some(content) = file.0.recv().await {
 81        cx.update(|cx| {
 82            cx.clear_bindings();
 83            settings::KeymapFile::load_defaults(cx);
 84            content.add(cx).log_err();
 85        });
 86    }
 87}
 88
 89#[cfg(test)]
 90mod tests {
 91    use super::*;
 92    use project::FakeFs;
 93    use settings::SoftWrap;
 94
 95    #[gpui::test]
 96    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
 97        let executor = cx.background();
 98        let fs = FakeFs::new(executor.clone());
 99
100        fs.save(
101            "/settings1.json".as_ref(),
102            &r#"
103            {
104                "buffer_font_size": 24,
105                "soft_wrap": "editor_width",
106                "language_overrides": {
107                    "Markdown": {
108                        "preferred_line_length": 100,
109                        "soft_wrap": "preferred_line_length"
110                    }
111                }
112            }
113            "#
114            .into(),
115        )
116        .await
117        .unwrap();
118
119        let source1 = WatchedJsonFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
120        let source2 = WatchedJsonFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
121        let source3 = WatchedJsonFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
122
123        let mut settings_rx = settings_from_files(
124            cx.read(Settings::test),
125            vec![source1, source2, source3],
126            ThemeRegistry::new((), cx.font_cache()),
127            cx.font_cache(),
128        );
129
130        let settings = settings_rx.next().await.unwrap();
131        let md_settings = settings.language_overrides.get("Markdown").unwrap();
132        assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
133        assert_eq!(settings.buffer_font_size, 24.0);
134        assert_eq!(settings.tab_size, 4);
135        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
136        assert_eq!(md_settings.preferred_line_length, Some(100));
137
138        fs.save(
139            "/settings2.json".as_ref(),
140            &r#"
141            {
142                "tab_size": 2,
143                "soft_wrap": "none",
144                "language_overrides": {
145                    "Markdown": {
146                        "preferred_line_length": 120
147                    }
148                }
149            }
150            "#
151            .into(),
152        )
153        .await
154        .unwrap();
155
156        let settings = settings_rx.next().await.unwrap();
157        let md_settings = settings.language_overrides.get("Markdown").unwrap();
158        assert_eq!(settings.soft_wrap, SoftWrap::None);
159        assert_eq!(settings.buffer_font_size, 24.0);
160        assert_eq!(settings.tab_size, 2);
161        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
162        assert_eq!(md_settings.preferred_line_length, Some(120));
163
164        fs.remove_file("/settings2.json".as_ref(), Default::default())
165            .await
166            .unwrap();
167
168        let settings = settings_rx.next().await.unwrap();
169        assert_eq!(settings.tab_size, 4);
170    }
171}