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}