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}