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::{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>(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| parse_json_with_comments(&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(
 80    mut file: WatchedJsonFile<KeymapFileContent>,
 81    mut cx: AsyncAppContext,
 82) {
 83    while let Some(content) = file.0.recv().await {
 84        cx.update(|cx| {
 85            cx.clear_bindings();
 86            settings::KeymapFileContent::load_defaults(cx);
 87            content.add(cx).log_err();
 88        });
 89    }
 90}
 91
 92#[cfg(test)]
 93mod tests {
 94    use super::*;
 95    use project::FakeFs;
 96    use settings::{EditorSettings, SoftWrap};
 97
 98    #[gpui::test]
 99    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
100        let executor = cx.background();
101        let fs = FakeFs::new(executor.clone());
102
103        fs.save(
104            "/settings1.json".as_ref(),
105            &r#"
106            {
107                "buffer_font_size": 24,
108                "soft_wrap": "editor_width",
109                "tab_size": 8,
110                "language_overrides": {
111                    "Markdown": {
112                        "tab_size": 2,
113                        "preferred_line_length": 100,
114                        "soft_wrap": "preferred_line_length"
115                    }
116                }
117            }
118            "#
119            .into(),
120            Default::default(),
121        )
122        .await
123        .unwrap();
124
125        let source1 = WatchedJsonFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
126        let source2 = WatchedJsonFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
127        let source3 = WatchedJsonFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
128
129        let settings = cx.read(Settings::test).with_language_defaults(
130            "JavaScript",
131            EditorSettings {
132                tab_size: Some(2.try_into().unwrap()),
133                ..Default::default()
134            },
135        );
136        let mut settings_rx = settings_from_files(
137            settings,
138            vec![source1, source2, source3],
139            ThemeRegistry::new((), cx.font_cache()),
140            cx.font_cache(),
141        );
142
143        let settings = settings_rx.next().await.unwrap();
144        assert_eq!(settings.buffer_font_size, 24.0);
145
146        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
147        assert_eq!(
148            settings.soft_wrap(Some("Markdown")),
149            SoftWrap::PreferredLineLength
150        );
151        assert_eq!(
152            settings.soft_wrap(Some("JavaScript")),
153            SoftWrap::EditorWidth
154        );
155
156        assert_eq!(settings.preferred_line_length(None), 80);
157        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
158        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
159
160        assert_eq!(settings.tab_size(None).get(), 8);
161        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
162        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
163
164        fs.save(
165            "/settings2.json".as_ref(),
166            &r#"
167            {
168                "tab_size": 2,
169                "soft_wrap": "none",
170                "language_overrides": {
171                    "Markdown": {
172                        "preferred_line_length": 120
173                    }
174                }
175            }
176            "#
177            .into(),
178            Default::default(),
179        )
180        .await
181        .unwrap();
182
183        let settings = settings_rx.next().await.unwrap();
184        assert_eq!(settings.buffer_font_size, 24.0);
185
186        assert_eq!(settings.soft_wrap(None), SoftWrap::None);
187        assert_eq!(
188            settings.soft_wrap(Some("Markdown")),
189            SoftWrap::PreferredLineLength
190        );
191        assert_eq!(settings.soft_wrap(Some("JavaScript")), SoftWrap::None);
192
193        assert_eq!(settings.preferred_line_length(None), 80);
194        assert_eq!(settings.preferred_line_length(Some("Markdown")), 120);
195        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
196
197        assert_eq!(settings.tab_size(None).get(), 2);
198        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
199        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 2);
200
201        fs.remove_file("/settings2.json".as_ref(), Default::default())
202            .await
203            .unwrap();
204
205        let settings = settings_rx.next().await.unwrap();
206        assert_eq!(settings.buffer_font_size, 24.0);
207
208        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
209        assert_eq!(
210            settings.soft_wrap(Some("Markdown")),
211            SoftWrap::PreferredLineLength
212        );
213        assert_eq!(
214            settings.soft_wrap(Some("JavaScript")),
215            SoftWrap::EditorWidth
216        );
217
218        assert_eq!(settings.preferred_line_length(None), 80);
219        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
220        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
221
222        assert_eq!(settings.tab_size(None).get(), 8);
223        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
224        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
225    }
226}