settings_file.rs

  1use futures::StreamExt;
  2use gpui::{executor, MutableAppContext};
  3use postage::sink::Sink as _;
  4use postage::{prelude::Stream, watch};
  5use project::Fs;
  6use serde::Deserialize;
  7use settings::{parse_json_with_comments, KeymapFileContent, Settings, SettingsFileContent};
  8use std::{path::Path, sync::Arc, time::Duration};
  9use theme::ThemeRegistry;
 10use util::ResultExt;
 11
 12#[derive(Clone)]
 13pub struct WatchedJsonFile<T>(pub 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    ///Loads the given watched JSON file. In the special case that the file is
 43    ///empty (ignoring whitespace) or is not a file, this will return T::default()
 44    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
 45        if !fs.is_file(path).await {
 46            return Some(T::default());
 47        }
 48
 49        fs.load(path).await.log_err().and_then(|data| {
 50            if data.trim().is_empty() {
 51                Some(T::default())
 52            } else {
 53                parse_json_with_comments(&data).log_err()
 54            }
 55        })
 56    }
 57}
 58
 59pub fn watch_settings_file(
 60    defaults: Settings,
 61    mut file: WatchedJsonFile<SettingsFileContent>,
 62    theme_registry: Arc<ThemeRegistry>,
 63    cx: &mut MutableAppContext,
 64) {
 65    settings_updated(&defaults, file.0.borrow().clone(), &theme_registry, cx);
 66    cx.spawn(|mut cx| async move {
 67        while let Some(content) = file.0.recv().await {
 68            cx.update(|cx| settings_updated(&defaults, content, &theme_registry, cx));
 69        }
 70    })
 71    .detach();
 72}
 73
 74pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
 75    cx.clear_bindings();
 76    settings::KeymapFileContent::load_defaults(cx);
 77    content.add_to_cx(cx).log_err();
 78}
 79
 80pub fn settings_updated(
 81    defaults: &Settings,
 82    content: SettingsFileContent,
 83    theme_registry: &Arc<ThemeRegistry>,
 84    cx: &mut MutableAppContext,
 85) {
 86    let mut settings = defaults.clone();
 87    settings.set_user_settings(content, theme_registry, cx.font_cache());
 88    cx.set_global(settings);
 89    cx.refresh_windows();
 90}
 91
 92pub fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
 93    cx.spawn(|mut cx| async move {
 94        while let Some(content) = file.0.recv().await {
 95            cx.update(|cx| keymap_updated(content, cx));
 96        }
 97    })
 98    .detach();
 99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use project::FakeFs;
105    use settings::{EditorSettings, SoftWrap};
106
107    #[gpui::test]
108    async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
109        let executor = cx.background();
110        let fs = FakeFs::new(executor.clone());
111        let font_cache = cx.font_cache();
112
113        fs.save(
114            "/settings.json".as_ref(),
115            &r#"
116            {
117                "buffer_font_size": 24,
118                "soft_wrap": "editor_width",
119                "tab_size": 8,
120                "language_overrides": {
121                    "Markdown": {
122                        "tab_size": 2,
123                        "preferred_line_length": 100,
124                        "soft_wrap": "preferred_line_length"
125                    }
126                }
127            }
128            "#
129            .into(),
130            Default::default(),
131        )
132        .await
133        .unwrap();
134
135        let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
136
137        let default_settings = cx.read(Settings::test).with_language_defaults(
138            "JavaScript",
139            EditorSettings {
140                tab_size: Some(2.try_into().unwrap()),
141                ..Default::default()
142            },
143        );
144        cx.update(|cx| {
145            watch_settings_file(
146                default_settings.clone(),
147                source,
148                ThemeRegistry::new((), font_cache),
149                cx,
150            )
151        });
152
153        cx.foreground().run_until_parked();
154        let settings = cx.read(|cx| cx.global::<Settings>().clone());
155        assert_eq!(settings.buffer_font_size, 24.0);
156
157        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
158        assert_eq!(
159            settings.soft_wrap(Some("Markdown")),
160            SoftWrap::PreferredLineLength
161        );
162        assert_eq!(
163            settings.soft_wrap(Some("JavaScript")),
164            SoftWrap::EditorWidth
165        );
166
167        assert_eq!(settings.preferred_line_length(None), 80);
168        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
169        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
170
171        assert_eq!(settings.tab_size(None).get(), 8);
172        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
173        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
174
175        fs.save(
176            "/settings.json".as_ref(),
177            &"(garbage)".into(),
178            Default::default(),
179        )
180        .await
181        .unwrap();
182        // fs.remove_file("/settings.json".as_ref(), Default::default())
183        //     .await
184        //     .unwrap();
185
186        cx.foreground().run_until_parked();
187        let settings = cx.read(|cx| cx.global::<Settings>().clone());
188        assert_eq!(settings.buffer_font_size, 24.0);
189
190        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
191        assert_eq!(
192            settings.soft_wrap(Some("Markdown")),
193            SoftWrap::PreferredLineLength
194        );
195        assert_eq!(
196            settings.soft_wrap(Some("JavaScript")),
197            SoftWrap::EditorWidth
198        );
199
200        assert_eq!(settings.preferred_line_length(None), 80);
201        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
202        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
203
204        assert_eq!(settings.tab_size(None).get(), 8);
205        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
206        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
207
208        fs.remove_file("/settings.json".as_ref(), Default::default())
209            .await
210            .unwrap();
211        cx.foreground().run_until_parked();
212        let settings = cx.read(|cx| cx.global::<Settings>().clone());
213        assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
214    }
215}