settings_file.rs

  1use fs::Fs;
  2use futures::StreamExt;
  3use gpui::{executor, MutableAppContext};
  4use postage::sink::Sink as _;
  5use postage::{prelude::Stream, watch};
  6use serde::Deserialize;
  7
  8use std::{path::Path, sync::Arc, time::Duration};
  9use theme::ThemeRegistry;
 10use util::ResultExt;
 11
 12use crate::{parse_json_with_comments, KeymapFileContent, Settings, SettingsFileContent};
 13
 14#[derive(Clone)]
 15pub struct WatchedJsonFile<T>(pub watch::Receiver<T>);
 16
 17// 1) Do the refactoring to pull WatchedJSON and fs out and into everything else
 18// 2) Scaffold this by making the basic structs we'll need SettingsFile::atomic_write_theme()
 19// 3) Fix the overeager settings writing, if that works, and there's no data loss, call it?
 20
 21impl<T> WatchedJsonFile<T>
 22where
 23    T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync,
 24{
 25    pub async fn new(
 26        fs: Arc<dyn Fs>,
 27        executor: &executor::Background,
 28        path: impl Into<Arc<Path>>,
 29    ) -> Self {
 30        let path = path.into();
 31        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
 32        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
 33        let (mut tx, rx) = watch::channel_with(settings);
 34        executor
 35            .spawn(async move {
 36                while events.next().await.is_some() {
 37                    if let Some(settings) = Self::load(fs.clone(), &path).await {
 38                        if tx.send(settings).await.is_err() {
 39                            break;
 40                        }
 41                    }
 42                }
 43            })
 44            .detach();
 45        Self(rx)
 46    }
 47
 48    ///Loads the given watched JSON file. In the special case that the file is
 49    ///empty (ignoring whitespace) or is not a file, this will return T::default()
 50    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
 51        if !fs.is_file(path).await {
 52            return Some(T::default());
 53        }
 54
 55        fs.load(path).await.log_err().and_then(|data| {
 56            if data.trim().is_empty() {
 57                Some(T::default())
 58            } else {
 59                parse_json_with_comments(&data).log_err()
 60            }
 61        })
 62    }
 63}
 64
 65pub fn watch_settings_file(
 66    defaults: Settings,
 67    mut file: WatchedJsonFile<SettingsFileContent>,
 68    theme_registry: Arc<ThemeRegistry>,
 69    cx: &mut MutableAppContext,
 70) {
 71    settings_updated(&defaults, file.0.borrow().clone(), &theme_registry, cx);
 72    cx.spawn(|mut cx| async move {
 73        while let Some(content) = file.0.recv().await {
 74            cx.update(|cx| settings_updated(&defaults, content, &theme_registry, cx));
 75        }
 76    })
 77    .detach();
 78}
 79
 80pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
 81    cx.clear_bindings();
 82    KeymapFileContent::load_defaults(cx);
 83    content.add_to_cx(cx).log_err();
 84}
 85
 86pub fn settings_updated(
 87    defaults: &Settings,
 88    content: SettingsFileContent,
 89    theme_registry: &Arc<ThemeRegistry>,
 90    cx: &mut MutableAppContext,
 91) {
 92    let mut settings = defaults.clone();
 93    settings.set_user_settings(content, theme_registry, cx.font_cache());
 94    cx.set_global(settings);
 95    cx.refresh_windows();
 96}
 97
 98pub fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
 99    cx.spawn(|mut cx| async move {
100        while let Some(content) = file.0.recv().await {
101            cx.update(|cx| keymap_updated(content, cx));
102        }
103    })
104    .detach();
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::{EditorSettings, SoftWrap};
111    use fs::FakeFs;
112
113    #[gpui::test]
114    async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
115        let executor = cx.background();
116        let fs = FakeFs::new(executor.clone());
117        let font_cache = cx.font_cache();
118
119        fs.save(
120            "/settings.json".as_ref(),
121            &r#"
122            {
123                "buffer_font_size": 24,
124                "soft_wrap": "editor_width",
125                "tab_size": 8,
126                "language_overrides": {
127                    "Markdown": {
128                        "tab_size": 2,
129                        "preferred_line_length": 100,
130                        "soft_wrap": "preferred_line_length"
131                    }
132                }
133            }
134            "#
135            .into(),
136            Default::default(),
137        )
138        .await
139        .unwrap();
140
141        let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
142
143        let default_settings = cx.read(Settings::test).with_language_defaults(
144            "JavaScript",
145            EditorSettings {
146                tab_size: Some(2.try_into().unwrap()),
147                ..Default::default()
148            },
149        );
150        cx.update(|cx| {
151            watch_settings_file(
152                default_settings.clone(),
153                source,
154                ThemeRegistry::new((), font_cache),
155                cx,
156            )
157        });
158
159        cx.foreground().run_until_parked();
160        let settings = cx.read(|cx| cx.global::<Settings>().clone());
161        assert_eq!(settings.buffer_font_size, 24.0);
162
163        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
164        assert_eq!(
165            settings.soft_wrap(Some("Markdown")),
166            SoftWrap::PreferredLineLength
167        );
168        assert_eq!(
169            settings.soft_wrap(Some("JavaScript")),
170            SoftWrap::EditorWidth
171        );
172
173        assert_eq!(settings.preferred_line_length(None), 80);
174        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
175        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
176
177        assert_eq!(settings.tab_size(None).get(), 8);
178        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
179        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
180
181        fs.save(
182            "/settings.json".as_ref(),
183            &"(garbage)".into(),
184            Default::default(),
185        )
186        .await
187        .unwrap();
188        // fs.remove_file("/settings.json".as_ref(), Default::default())
189        //     .await
190        //     .unwrap();
191
192        cx.foreground().run_until_parked();
193        let settings = cx.read(|cx| cx.global::<Settings>().clone());
194        assert_eq!(settings.buffer_font_size, 24.0);
195
196        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
197        assert_eq!(
198            settings.soft_wrap(Some("Markdown")),
199            SoftWrap::PreferredLineLength
200        );
201        assert_eq!(
202            settings.soft_wrap(Some("JavaScript")),
203            SoftWrap::EditorWidth
204        );
205
206        assert_eq!(settings.preferred_line_length(None), 80);
207        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
208        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
209
210        assert_eq!(settings.tab_size(None).get(), 8);
211        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
212        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
213
214        fs.remove_file("/settings.json".as_ref(), Default::default())
215            .await
216            .unwrap();
217        cx.foreground().run_until_parked();
218        let settings = cx.read(|cx| cx.global::<Settings>().clone());
219        assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
220    }
221}