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