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::{
13 parse_json_with_comments, write_top_level_setting, KeymapFileContent, Settings,
14 SettingsFileContent,
15};
16
17// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
18// And instant updates in the Zed editor
19#[derive(Clone)]
20pub struct SettingsFile {
21 path: &'static Path,
22 fs: Arc<dyn Fs>,
23}
24
25impl SettingsFile {
26 pub fn new(path: &'static Path, fs: Arc<dyn Fs>) -> Self {
27 SettingsFile { path, fs }
28 }
29
30 pub async fn rewrite_settings_file<F>(&self, f: F) -> anyhow::Result<()>
31 where
32 F: Fn(String) -> String,
33 {
34 let content = self.fs.load(self.path).await?;
35
36 let new_settings = f(content);
37
38 self.fs
39 .atomic_write(self.path.to_path_buf(), new_settings)
40 .await?;
41
42 Ok(())
43 }
44}
45
46pub fn write_setting(key: &'static str, val: String, cx: &mut MutableAppContext) {
47 let settings_file = cx.global::<SettingsFile>().clone();
48 cx.background()
49 .spawn(async move {
50 settings_file
51 .rewrite_settings_file(|settings| write_top_level_setting(settings, key, &val))
52 .await
53 })
54 .detach_and_log_err(cx);
55}
56
57#[derive(Clone)]
58pub struct WatchedJsonFile<T>(pub watch::Receiver<T>);
59
60impl<T> WatchedJsonFile<T>
61where
62 T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync,
63{
64 pub async fn new(
65 fs: Arc<dyn Fs>,
66 executor: &executor::Background,
67 path: impl Into<Arc<Path>>,
68 ) -> Self {
69 let path = path.into();
70 let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
71 let mut events = fs.watch(&path, Duration::from_millis(500)).await;
72 let (mut tx, rx) = watch::channel_with(settings);
73 executor
74 .spawn(async move {
75 while events.next().await.is_some() {
76 if let Some(settings) = Self::load(fs.clone(), &path).await {
77 if tx.send(settings).await.is_err() {
78 break;
79 }
80 }
81 }
82 })
83 .detach();
84 Self(rx)
85 }
86
87 ///Loads the given watched JSON file. In the special case that the file is
88 ///empty (ignoring whitespace) or is not a file, this will return T::default()
89 async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
90 if !fs.is_file(path).await {
91 return Some(T::default());
92 }
93
94 fs.load(path).await.log_err().and_then(|data| {
95 if data.trim().is_empty() {
96 Some(T::default())
97 } else {
98 parse_json_with_comments(&data).log_err()
99 }
100 })
101 }
102}
103
104pub fn watch_settings_file(
105 defaults: Settings,
106 mut file: WatchedJsonFile<SettingsFileContent>,
107 theme_registry: Arc<ThemeRegistry>,
108 cx: &mut MutableAppContext,
109) {
110 settings_updated(&defaults, file.0.borrow().clone(), &theme_registry, cx);
111 cx.spawn(|mut cx| async move {
112 while let Some(content) = file.0.recv().await {
113 cx.update(|cx| settings_updated(&defaults, content, &theme_registry, cx));
114 }
115 })
116 .detach();
117}
118
119pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
120 cx.clear_bindings();
121 KeymapFileContent::load_defaults(cx);
122 content.add_to_cx(cx).log_err();
123}
124
125pub fn settings_updated(
126 defaults: &Settings,
127 content: SettingsFileContent,
128 theme_registry: &Arc<ThemeRegistry>,
129 cx: &mut MutableAppContext,
130) {
131 let mut settings = defaults.clone();
132 settings.set_user_settings(content, theme_registry, cx.font_cache());
133 cx.set_global(settings);
134 cx.refresh_windows();
135}
136
137pub fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
138 cx.spawn(|mut cx| async move {
139 while let Some(content) = file.0.recv().await {
140 cx.update(|cx| keymap_updated(content, cx));
141 }
142 })
143 .detach();
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::{EditorSettings, SoftWrap};
150 use fs::FakeFs;
151
152 #[gpui::test]
153 async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
154 let executor = cx.background();
155 let fs = FakeFs::new(executor.clone());
156 let font_cache = cx.font_cache();
157
158 fs.save(
159 "/settings.json".as_ref(),
160 &r#"
161 {
162 "buffer_font_size": 24,
163 "soft_wrap": "editor_width",
164 "tab_size": 8,
165 "language_overrides": {
166 "Markdown": {
167 "tab_size": 2,
168 "preferred_line_length": 100,
169 "soft_wrap": "preferred_line_length"
170 }
171 }
172 }
173 "#
174 .into(),
175 Default::default(),
176 )
177 .await
178 .unwrap();
179
180 let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
181
182 let default_settings = cx.read(Settings::test).with_language_defaults(
183 "JavaScript",
184 EditorSettings {
185 tab_size: Some(2.try_into().unwrap()),
186 ..Default::default()
187 },
188 );
189 cx.update(|cx| {
190 watch_settings_file(
191 default_settings.clone(),
192 source,
193 ThemeRegistry::new((), font_cache),
194 cx,
195 )
196 });
197
198 cx.foreground().run_until_parked();
199 let settings = cx.read(|cx| cx.global::<Settings>().clone());
200 assert_eq!(settings.buffer_font_size, 24.0);
201
202 assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
203 assert_eq!(
204 settings.soft_wrap(Some("Markdown")),
205 SoftWrap::PreferredLineLength
206 );
207 assert_eq!(
208 settings.soft_wrap(Some("JavaScript")),
209 SoftWrap::EditorWidth
210 );
211
212 assert_eq!(settings.preferred_line_length(None), 80);
213 assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
214 assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
215
216 assert_eq!(settings.tab_size(None).get(), 8);
217 assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
218 assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
219
220 fs.save(
221 "/settings.json".as_ref(),
222 &"(garbage)".into(),
223 Default::default(),
224 )
225 .await
226 .unwrap();
227 // fs.remove_file("/settings.json".as_ref(), Default::default())
228 // .await
229 // .unwrap();
230
231 cx.foreground().run_until_parked();
232 let settings = cx.read(|cx| cx.global::<Settings>().clone());
233 assert_eq!(settings.buffer_font_size, 24.0);
234
235 assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
236 assert_eq!(
237 settings.soft_wrap(Some("Markdown")),
238 SoftWrap::PreferredLineLength
239 );
240 assert_eq!(
241 settings.soft_wrap(Some("JavaScript")),
242 SoftWrap::EditorWidth
243 );
244
245 assert_eq!(settings.preferred_line_length(None), 80);
246 assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
247 assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
248
249 assert_eq!(settings.tab_size(None).get(), 8);
250 assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
251 assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
252
253 fs.remove_file("/settings.json".as_ref(), Default::default())
254 .await
255 .unwrap();
256 cx.foreground().run_until_parked();
257 let settings = cx.read(|cx| cx.global::<Settings>().clone());
258 assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
259 }
260}