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