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, elements::*, Action, Entity, View, ViewContext, WindowContext};
84 use theme::ThemeRegistry;
85
86 struct TestView;
87
88 impl Entity for TestView {
89 type Event = ();
90 }
91
92 impl View for TestView {
93 fn ui_name() -> &'static str {
94 "TestView"
95 }
96
97 fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
98 Empty::new().into_any()
99 }
100 }
101
102 #[gpui::test]
103 async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
104 let executor = cx.background();
105 let fs = FakeFs::new(executor.clone());
106 let font_cache = cx.font_cache();
107
108 actions!(test, [A, B]);
109 // From the Atom keymap
110 actions!(workspace, [ActivatePreviousPane]);
111 // From the JetBrains keymap
112 actions!(pane, [ActivatePrevItem]);
113
114 fs.save(
115 "/settings.json".as_ref(),
116 &r#"
117 {
118 "base_keymap": "Atom"
119 }
120 "#
121 .into(),
122 Default::default(),
123 )
124 .await
125 .unwrap();
126
127 fs.save(
128 "/keymap.json".as_ref(),
129 &r#"
130 [
131 {
132 "bindings": {
133 "backspace": "test::A"
134 }
135 }
136 ]
137 "#
138 .into(),
139 Default::default(),
140 )
141 .await
142 .unwrap();
143
144 let settings_file =
145 WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
146 let keymaps_file =
147 WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
148
149 let default_settings = cx.read(Settings::test);
150
151 cx.update(|cx| {
152 cx.add_global_action(|_: &A, _cx| {});
153 cx.add_global_action(|_: &B, _cx| {});
154 cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
155 cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
156 watch_files(
157 default_settings,
158 settings_file,
159 ThemeRegistry::new((), font_cache),
160 keymaps_file,
161 cx,
162 )
163 });
164
165 cx.foreground().run_until_parked();
166
167 let (window_id, _view) = cx.add_window(|_| TestView);
168
169 // Test loading the keymap base at all
170 cx.read_window(window_id, |cx| {
171 assert_key_bindings_for(
172 cx,
173 vec![("backspace", &A), ("k", &ActivatePreviousPane)],
174 line!(),
175 );
176 });
177
178 // Test modifying the users keymap, while retaining the base keymap
179 fs.save(
180 "/keymap.json".as_ref(),
181 &r#"
182 [
183 {
184 "bindings": {
185 "backspace": "test::B"
186 }
187 }
188 ]
189 "#
190 .into(),
191 Default::default(),
192 )
193 .await
194 .unwrap();
195
196 cx.foreground().run_until_parked();
197
198 cx.read_window(window_id, |cx| {
199 assert_key_bindings_for(
200 cx,
201 vec![("backspace", &B), ("k", &ActivatePreviousPane)],
202 line!(),
203 );
204 });
205
206 // Test modifying the base, while retaining the users keymap
207 fs.save(
208 "/settings.json".as_ref(),
209 &r#"
210 {
211 "base_keymap": "JetBrains"
212 }
213 "#
214 .into(),
215 Default::default(),
216 )
217 .await
218 .unwrap();
219
220 cx.foreground().run_until_parked();
221
222 cx.read_window(window_id, |cx| {
223 assert_key_bindings_for(
224 cx,
225 vec![("backspace", &B), ("[", &ActivatePrevItem)],
226 line!(),
227 );
228 });
229 }
230
231 fn assert_key_bindings_for<'a>(
232 cx: &WindowContext,
233 actions: Vec<(&'static str, &'a dyn Action)>,
234 line: u32,
235 ) {
236 for (key, action) in actions {
237 // assert that...
238 assert!(
239 cx.available_actions(0).any(|(_, bound_action, b)| {
240 // action names match...
241 bound_action.name() == action.name()
242 && bound_action.namespace() == action.namespace()
243 // and key strokes contain the given key
244 && b.iter()
245 .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
246 }),
247 "On {} Failed to find {} with key binding {}",
248 line,
249 action.name(),
250 key
251 );
252 }
253 }
254
255 #[gpui::test]
256 async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
257 let executor = cx.background();
258 let fs = FakeFs::new(executor.clone());
259 let font_cache = cx.font_cache();
260
261 fs.save(
262 "/settings.json".as_ref(),
263 &r#"
264 {
265 "buffer_font_size": 24,
266 "soft_wrap": "editor_width",
267 "tab_size": 8,
268 "language_overrides": {
269 "Markdown": {
270 "tab_size": 2,
271 "preferred_line_length": 100,
272 "soft_wrap": "preferred_line_length"
273 }
274 }
275 }
276 "#
277 .into(),
278 Default::default(),
279 )
280 .await
281 .unwrap();
282
283 let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
284
285 let default_settings = cx.read(Settings::test).with_language_defaults(
286 "JavaScript",
287 EditorSettings {
288 tab_size: Some(2.try_into().unwrap()),
289 ..Default::default()
290 },
291 );
292 cx.update(|cx| {
293 watch_settings_file(
294 default_settings.clone(),
295 source,
296 ThemeRegistry::new((), font_cache),
297 cx,
298 )
299 });
300
301 cx.foreground().run_until_parked();
302 let settings = cx.read(|cx| cx.global::<Settings>().clone());
303 assert_eq!(settings.buffer_font_size, 24.0);
304
305 assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
306 assert_eq!(
307 settings.soft_wrap(Some("Markdown")),
308 SoftWrap::PreferredLineLength
309 );
310 assert_eq!(
311 settings.soft_wrap(Some("JavaScript")),
312 SoftWrap::EditorWidth
313 );
314
315 assert_eq!(settings.preferred_line_length(None), 80);
316 assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
317 assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
318
319 assert_eq!(settings.tab_size(None).get(), 8);
320 assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
321 assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
322
323 fs.save(
324 "/settings.json".as_ref(),
325 &"(garbage)".into(),
326 Default::default(),
327 )
328 .await
329 .unwrap();
330 // fs.remove_file("/settings.json".as_ref(), Default::default())
331 // .await
332 // .unwrap();
333
334 cx.foreground().run_until_parked();
335 let settings = cx.read(|cx| cx.global::<Settings>().clone());
336 assert_eq!(settings.buffer_font_size, 24.0);
337
338 assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
339 assert_eq!(
340 settings.soft_wrap(Some("Markdown")),
341 SoftWrap::PreferredLineLength
342 );
343 assert_eq!(
344 settings.soft_wrap(Some("JavaScript")),
345 SoftWrap::EditorWidth
346 );
347
348 assert_eq!(settings.preferred_line_length(None), 80);
349 assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
350 assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
351
352 assert_eq!(settings.tab_size(None).get(), 8);
353 assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
354 assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
355
356 fs.remove_file("/settings.json".as_ref(), Default::default())
357 .await
358 .unwrap();
359 cx.foreground().run_until_parked();
360 let settings = cx.read(|cx| cx.global::<Settings>().clone());
361 assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
362 }
363}