1use crate::{update_settings_file, watched_json::WatchedJsonFile, SettingsFileContent};
2use anyhow::Result;
3use assets::Assets;
4use fs::Fs;
5use gpui::{AppContext, AssetSource};
6use std::{io::ErrorKind, path::Path, sync::Arc};
7
8// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
9// And instant updates in the Zed editor
10#[derive(Clone)]
11pub struct SettingsFile {
12 path: &'static Path,
13 settings_file_content: WatchedJsonFile<SettingsFileContent>,
14 fs: Arc<dyn Fs>,
15}
16
17impl SettingsFile {
18 pub fn new(
19 path: &'static Path,
20 settings_file_content: WatchedJsonFile<SettingsFileContent>,
21 fs: Arc<dyn Fs>,
22 ) -> Self {
23 SettingsFile {
24 path,
25 settings_file_content,
26 fs,
27 }
28 }
29
30 async fn load_settings(path: &Path, fs: &Arc<dyn Fs>) -> Result<String> {
31 match fs.load(path).await {
32 result @ Ok(_) => result,
33 Err(err) => {
34 if let Some(e) = err.downcast_ref::<std::io::Error>() {
35 if e.kind() == ErrorKind::NotFound {
36 return Ok(std::str::from_utf8(
37 Assets
38 .load("settings/initial_user_settings.json")
39 .unwrap()
40 .as_ref(),
41 )
42 .unwrap()
43 .to_string());
44 }
45 }
46 return Err(err);
47 }
48 }
49 }
50
51 pub fn update(
52 cx: &mut AppContext,
53 update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
54 ) {
55 let this = cx.global::<SettingsFile>();
56
57 let current_file_content = this.settings_file_content.current();
58
59 let fs = this.fs.clone();
60 let path = this.path.clone();
61
62 cx.background()
63 .spawn(async move {
64 let old_text = SettingsFile::load_settings(path, &fs).await?;
65
66 let new_text = update_settings_file(old_text, current_file_content, update);
67
68 fs.atomic_write(path.to_path_buf(), new_text).await?;
69
70 Ok(()) as Result<()>
71 })
72 .detach_and_log_err(cx);
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::{
80 watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
81 };
82 use fs::FakeFs;
83 use gpui::{actions, Action};
84 use theme::ThemeRegistry;
85
86 #[gpui::test]
87 async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
88 let executor = cx.background();
89 let fs = FakeFs::new(executor.clone());
90 let font_cache = cx.font_cache();
91
92 actions!(test, [A, B]);
93 // From the Atom keymap
94 actions!(workspace, [ActivatePreviousPane]);
95 // From the JetBrains keymap
96 actions!(pane, [ActivatePrevItem]);
97
98 fs.save(
99 "/settings.json".as_ref(),
100 &r#"
101 {
102 "base_keymap": "Atom"
103 }
104 "#
105 .into(),
106 Default::default(),
107 )
108 .await
109 .unwrap();
110
111 fs.save(
112 "/keymap.json".as_ref(),
113 &r#"
114 [
115 {
116 "bindings": {
117 "backspace": "test::A"
118 }
119 }
120 ]
121 "#
122 .into(),
123 Default::default(),
124 )
125 .await
126 .unwrap();
127
128 let settings_file =
129 WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
130 let keymaps_file =
131 WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
132
133 let default_settings = cx.read(Settings::test);
134
135 cx.update(|cx| {
136 cx.add_global_action(|_: &A, _cx| {});
137 cx.add_global_action(|_: &B, _cx| {});
138 cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
139 cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
140 watch_files(
141 default_settings,
142 settings_file,
143 ThemeRegistry::new((), font_cache),
144 keymaps_file,
145 cx,
146 )
147 });
148
149 cx.foreground().run_until_parked();
150
151 // Test loading the keymap base at all
152 cx.update(|cx| {
153 assert_key_bindings_for(
154 cx,
155 vec![("backspace", &A), ("k", &ActivatePreviousPane)],
156 line!(),
157 );
158 });
159
160 // Test modifying the users keymap, while retaining the base keymap
161 fs.save(
162 "/keymap.json".as_ref(),
163 &r#"
164 [
165 {
166 "bindings": {
167 "backspace": "test::B"
168 }
169 }
170 ]
171 "#
172 .into(),
173 Default::default(),
174 )
175 .await
176 .unwrap();
177
178 cx.foreground().run_until_parked();
179
180 cx.update(|cx| {
181 assert_key_bindings_for(
182 cx,
183 vec![("backspace", &B), ("k", &ActivatePreviousPane)],
184 line!(),
185 );
186 });
187
188 // Test modifying the base, while retaining the users keymap
189 fs.save(
190 "/settings.json".as_ref(),
191 &r#"
192 {
193 "base_keymap": "JetBrains"
194 }
195 "#
196 .into(),
197 Default::default(),
198 )
199 .await
200 .unwrap();
201
202 cx.foreground().run_until_parked();
203
204 cx.update(|cx| {
205 assert_key_bindings_for(
206 cx,
207 vec![("backspace", &B), ("[", &ActivatePrevItem)],
208 line!(),
209 );
210 });
211 }
212
213 fn assert_key_bindings_for<'a>(
214 cx: &mut AppContext,
215 actions: Vec<(&'static str, &'a dyn Action)>,
216 line: u32,
217 ) {
218 for (key, action) in actions {
219 // assert that...
220 assert!(
221 cx.available_actions(0, 0).any(|(_, bound_action, b)| {
222 // action names match...
223 bound_action.name() == action.name()
224 && bound_action.namespace() == action.namespace()
225 // and key strokes contain the given key
226 && b.iter()
227 .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
228 }),
229 "On {} Failed to find {} with key binding {}",
230 line,
231 action.name(),
232 key
233 );
234 }
235 }
236
237 #[gpui::test]
238 async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
239 let executor = cx.background();
240 let fs = FakeFs::new(executor.clone());
241 let font_cache = cx.font_cache();
242
243 fs.save(
244 "/settings.json".as_ref(),
245 &r#"
246 {
247 "buffer_font_size": 24,
248 "soft_wrap": "editor_width",
249 "tab_size": 8,
250 "language_overrides": {
251 "Markdown": {
252 "tab_size": 2,
253 "preferred_line_length": 100,
254 "soft_wrap": "preferred_line_length"
255 }
256 }
257 }
258 "#
259 .into(),
260 Default::default(),
261 )
262 .await
263 .unwrap();
264
265 let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
266
267 let default_settings = cx.read(Settings::test).with_language_defaults(
268 "JavaScript",
269 EditorSettings {
270 tab_size: Some(2.try_into().unwrap()),
271 ..Default::default()
272 },
273 );
274 cx.update(|cx| {
275 watch_settings_file(
276 default_settings.clone(),
277 source,
278 ThemeRegistry::new((), font_cache),
279 cx,
280 )
281 });
282
283 cx.foreground().run_until_parked();
284 let settings = cx.read(|cx| cx.global::<Settings>().clone());
285 assert_eq!(settings.buffer_font_size, 24.0);
286
287 assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
288 assert_eq!(
289 settings.soft_wrap(Some("Markdown")),
290 SoftWrap::PreferredLineLength
291 );
292 assert_eq!(
293 settings.soft_wrap(Some("JavaScript")),
294 SoftWrap::EditorWidth
295 );
296
297 assert_eq!(settings.preferred_line_length(None), 80);
298 assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
299 assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
300
301 assert_eq!(settings.tab_size(None).get(), 8);
302 assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
303 assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
304
305 fs.save(
306 "/settings.json".as_ref(),
307 &"(garbage)".into(),
308 Default::default(),
309 )
310 .await
311 .unwrap();
312 // fs.remove_file("/settings.json".as_ref(), Default::default())
313 // .await
314 // .unwrap();
315
316 cx.foreground().run_until_parked();
317 let settings = cx.read(|cx| cx.global::<Settings>().clone());
318 assert_eq!(settings.buffer_font_size, 24.0);
319
320 assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
321 assert_eq!(
322 settings.soft_wrap(Some("Markdown")),
323 SoftWrap::PreferredLineLength
324 );
325 assert_eq!(
326 settings.soft_wrap(Some("JavaScript")),
327 SoftWrap::EditorWidth
328 );
329
330 assert_eq!(settings.preferred_line_length(None), 80);
331 assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
332 assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
333
334 assert_eq!(settings.tab_size(None).get(), 8);
335 assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
336 assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
337
338 fs.remove_file("/settings.json".as_ref(), Default::default())
339 .await
340 .unwrap();
341 cx.foreground().run_until_parked();
342 let settings = cx.read(|cx| cx.global::<Settings>().clone());
343 assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
344 }
345}