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}