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}