settings_file.rs

  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}