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}