settings_file.rs

  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}