settings_file.rs

  1use crate::{
  2    settings_store::parse_json_with_comments, settings_store::SettingsStore, KeymapFileContent,
  3    Settings, SettingsFileContent, DEFAULT_SETTINGS_ASSET_PATH,
  4};
  5use anyhow::Result;
  6use assets::Assets;
  7use fs::Fs;
  8use futures::{channel::mpsc, StreamExt};
  9use gpui::{executor::Background, AppContext, AssetSource};
 10use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
 11use util::{paths, ResultExt};
 12
 13pub fn default_settings() -> Cow<'static, str> {
 14    match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
 15        Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
 16        Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
 17    }
 18}
 19
 20#[cfg(any(test, feature = "test-support"))]
 21pub fn test_settings() -> String {
 22    let mut value =
 23        parse_json_with_comments::<serde_json::Value>(default_settings().as_ref()).unwrap();
 24    util::merge_non_null_json_value_into(
 25        serde_json::json!({
 26            "buffer_font_family": "Courier",
 27            "buffer_font_features": {},
 28            "default_buffer_font_size": 14,
 29            "preferred_line_length": 80,
 30            "theme": theme::EMPTY_THEME_NAME,
 31        }),
 32        &mut value,
 33    );
 34    serde_json::to_string(&value).unwrap()
 35}
 36
 37pub fn watch_config_file(
 38    executor: Arc<Background>,
 39    fs: Arc<dyn Fs>,
 40    path: PathBuf,
 41) -> mpsc::UnboundedReceiver<String> {
 42    let (tx, rx) = mpsc::unbounded();
 43    executor
 44        .spawn(async move {
 45            let events = fs.watch(&path, Duration::from_millis(100)).await;
 46            futures::pin_mut!(events);
 47            loop {
 48                if let Ok(contents) = fs.load(&path).await {
 49                    if !tx.unbounded_send(contents).is_ok() {
 50                        break;
 51                    }
 52                }
 53                if events.next().await.is_none() {
 54                    break;
 55                }
 56            }
 57        })
 58        .detach();
 59    rx
 60}
 61
 62pub fn handle_keymap_file_changes(
 63    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
 64    cx: &mut AppContext,
 65) {
 66    cx.spawn(move |mut cx| async move {
 67        let mut settings_subscription = None;
 68        while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
 69            if let Ok(keymap_content) =
 70                parse_json_with_comments::<KeymapFileContent>(&user_keymap_content)
 71            {
 72                cx.update(|cx| {
 73                    cx.clear_bindings();
 74                    KeymapFileContent::load_defaults(cx);
 75                    keymap_content.clone().add_to_cx(cx).log_err();
 76                });
 77
 78                let mut old_base_keymap = cx.read(|cx| cx.global::<Settings>().base_keymap.clone());
 79                drop(settings_subscription);
 80                settings_subscription = Some(cx.update(|cx| {
 81                    cx.observe_global::<Settings, _>(move |cx| {
 82                        let settings = cx.global::<Settings>();
 83                        if settings.base_keymap != old_base_keymap {
 84                            old_base_keymap = settings.base_keymap.clone();
 85
 86                            cx.clear_bindings();
 87                            KeymapFileContent::load_defaults(cx);
 88                            keymap_content.clone().add_to_cx(cx).log_err();
 89                        }
 90                    })
 91                    .detach();
 92                }));
 93            }
 94        }
 95    })
 96    .detach();
 97}
 98
 99pub fn handle_settings_file_changes(
100    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
101    cx: &mut AppContext,
102) {
103    let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap();
104    cx.update_global::<SettingsStore, _, _>(|store, cx| {
105        store
106            .set_user_settings(&user_settings_content, cx)
107            .log_err();
108
109        // TODO - remove the Settings global, use the SettingsStore instead.
110        store.register_setting::<Settings>(cx);
111        cx.set_global(store.get::<Settings>(None).clone());
112    });
113    cx.spawn(move |mut cx| async move {
114        while let Some(user_settings_content) = user_settings_file_rx.next().await {
115            cx.update(|cx| {
116                cx.update_global::<SettingsStore, _, _>(|store, cx| {
117                    store
118                        .set_user_settings(&user_settings_content, cx)
119                        .log_err();
120
121                    // TODO - remove the Settings global, use the SettingsStore instead.
122                    cx.set_global(store.get::<Settings>(None).clone());
123                });
124            });
125        }
126    })
127    .detach();
128}
129
130async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
131    match fs.load(&paths::SETTINGS).await {
132        result @ Ok(_) => result,
133        Err(err) => {
134            if let Some(e) = err.downcast_ref::<std::io::Error>() {
135                if e.kind() == ErrorKind::NotFound {
136                    return Ok(Settings::initial_user_settings_content(&Assets).to_string());
137                }
138            }
139            return Err(err);
140        }
141    }
142}
143
144pub fn update_settings_file(
145    fs: Arc<dyn Fs>,
146    cx: &mut AppContext,
147    update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
148) {
149    cx.spawn(|cx| async move {
150        let old_text = cx
151            .background()
152            .spawn({
153                let fs = fs.clone();
154                async move { load_settings(&fs).await }
155            })
156            .await?;
157
158        let edits = cx.read(|cx| {
159            cx.global::<SettingsStore>()
160                .update::<Settings>(&old_text, update)
161        });
162
163        let mut new_text = old_text;
164        for (range, replacement) in edits.into_iter().rev() {
165            new_text.replace_range(range, &replacement);
166        }
167
168        cx.background()
169            .spawn(async move { fs.atomic_write(paths::SETTINGS.clone(), new_text).await })
170            .await?;
171        anyhow::Ok(())
172    })
173    .detach_and_log_err(cx);
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use fs::FakeFs;
180    use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext};
181    use theme::ThemeRegistry;
182
183    struct TestView;
184
185    impl Entity for TestView {
186        type Event = ();
187    }
188
189    impl View for TestView {
190        fn ui_name() -> &'static str {
191            "TestView"
192        }
193
194        fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
195            Empty::new().into_any()
196        }
197    }
198
199    #[gpui::test]
200    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
201        let executor = cx.background();
202        let fs = FakeFs::new(executor.clone());
203
204        actions!(test, [A, B]);
205        // From the Atom keymap
206        actions!(workspace, [ActivatePreviousPane]);
207        // From the JetBrains keymap
208        actions!(pane, [ActivatePrevItem]);
209
210        fs.save(
211            "/settings.json".as_ref(),
212            &r#"
213            {
214                "base_keymap": "Atom"
215            }
216            "#
217            .into(),
218            Default::default(),
219        )
220        .await
221        .unwrap();
222
223        fs.save(
224            "/keymap.json".as_ref(),
225            &r#"
226            [
227                {
228                    "bindings": {
229                        "backspace": "test::A"
230                    }
231                }
232            ]
233            "#
234            .into(),
235            Default::default(),
236        )
237        .await
238        .unwrap();
239
240        cx.update(|cx| {
241            let mut store = SettingsStore::default();
242            store.set_default_settings(&test_settings(), cx).unwrap();
243            cx.set_global(store);
244            cx.set_global(ThemeRegistry::new(Assets, cx.font_cache().clone()));
245            cx.add_global_action(|_: &A, _cx| {});
246            cx.add_global_action(|_: &B, _cx| {});
247            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
248            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
249
250            let settings_rx = watch_config_file(
251                executor.clone(),
252                fs.clone(),
253                PathBuf::from("/settings.json"),
254            );
255            let keymap_rx =
256                watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
257
258            handle_keymap_file_changes(keymap_rx, cx);
259            handle_settings_file_changes(settings_rx, cx);
260        });
261
262        cx.foreground().run_until_parked();
263
264        let (window_id, _view) = cx.add_window(|_| TestView);
265
266        // Test loading the keymap base at all
267        assert_key_bindings_for(
268            window_id,
269            cx,
270            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
271            line!(),
272        );
273
274        // Test modifying the users keymap, while retaining the base keymap
275        fs.save(
276            "/keymap.json".as_ref(),
277            &r#"
278            [
279                {
280                    "bindings": {
281                        "backspace": "test::B"
282                    }
283                }
284            ]
285            "#
286            .into(),
287            Default::default(),
288        )
289        .await
290        .unwrap();
291
292        cx.foreground().run_until_parked();
293
294        assert_key_bindings_for(
295            window_id,
296            cx,
297            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
298            line!(),
299        );
300
301        // Test modifying the base, while retaining the users keymap
302        fs.save(
303            "/settings.json".as_ref(),
304            &r#"
305            {
306                "base_keymap": "JetBrains"
307            }
308            "#
309            .into(),
310            Default::default(),
311        )
312        .await
313        .unwrap();
314
315        cx.foreground().run_until_parked();
316
317        assert_key_bindings_for(
318            window_id,
319            cx,
320            vec![("backspace", &B), ("[", &ActivatePrevItem)],
321            line!(),
322        );
323    }
324
325    fn assert_key_bindings_for<'a>(
326        window_id: usize,
327        cx: &TestAppContext,
328        actions: Vec<(&'static str, &'a dyn Action)>,
329        line: u32,
330    ) {
331        for (key, action) in actions {
332            // assert that...
333            assert!(
334                cx.available_actions(window_id, 0)
335                    .into_iter()
336                    .any(|(_, bound_action, b)| {
337                        // action names match...
338                        bound_action.name() == action.name()
339                    && bound_action.namespace() == action.namespace()
340                    // and key strokes contain the given key
341                    && b.iter()
342                        .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
343                    }),
344                "On {} Failed to find {} with key binding {}",
345                line,
346                action.name(),
347                key
348            );
349        }
350    }
351}