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}