settings_file.rs

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