settings_file.rs

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