settings_file.rs

  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}