Settings/keymap backup path next to files + update notification messages (#24517)

Michael Sloan created

Before:


![image](https://github.com/user-attachments/assets/5b7d8677-b0db-4a66-ac30-e4751ba4182d)

After:


![image](https://github.com/user-attachments/assets/94743bc2-2902-43a3-8d6e-e0e0e6e469ec)

Release Notes:

- N/A

Change summary

crates/paths/src/paths.rs             |  12 +++
crates/settings/src/keymap_file.rs    |  18 ++--
crates/settings/src/settings_store.rs |  27 +++---
crates/workspace/src/notifications.rs |  16 ++++
crates/zed/src/zed.rs                 | 105 ++++++++++++++++++----------
5 files changed, 116 insertions(+), 62 deletions(-)

Detailed changes

crates/paths/src/paths.rs 🔗

@@ -145,12 +145,24 @@ pub fn settings_file() -> &'static PathBuf {
     SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json"))
 }
 
+/// Returns the path to the `settings_backup.json` file.
+pub fn settings_backup_file() -> &'static PathBuf {
+    static SETTINGS_FILE: OnceLock<PathBuf> = OnceLock::new();
+    SETTINGS_FILE.get_or_init(|| config_dir().join("settings_backup.json"))
+}
+
 /// Returns the path to the `keymap.json` file.
 pub fn keymap_file() -> &'static PathBuf {
     static KEYMAP_FILE: OnceLock<PathBuf> = OnceLock::new();
     KEYMAP_FILE.get_or_init(|| config_dir().join("keymap.json"))
 }
 
+/// Returns the path to the `keymap_backup.json` file.
+pub fn keymap_backup_file() -> &'static PathBuf {
+    static KEYMAP_FILE: OnceLock<PathBuf> = OnceLock::new();
+    KEYMAP_FILE.get_or_init(|| config_dir().join("keymap_backup.json"))
+}
+
 /// Returns the path to the `tasks.json` file.
 pub fn tasks_file() -> &'static PathBuf {
     static TASKS_FILE: OnceLock<PathBuf> = OnceLock::new();

crates/settings/src/keymap_file.rs 🔗

@@ -588,24 +588,24 @@ impl KeymapFile {
         let Some(new_text) = migrate_keymap(&old_text) else {
             return Ok(());
         };
-        let initial_path = paths::keymap_file().as_path();
-        if fs.is_file(initial_path).await {
-            let backup_path = paths::home_dir().join(".zed_keymap_backup");
-            fs.atomic_write(backup_path, old_text)
+        let keymap_path = paths::keymap_file().as_path();
+        if fs.is_file(keymap_path).await {
+            fs.atomic_write(paths::keymap_backup_file().to_path_buf(), old_text)
                 .await
                 .with_context(|| {
                     "Failed to create settings backup in home directory".to_string()
                 })?;
-            let resolved_path = fs.canonicalize(initial_path).await.with_context(|| {
-                format!("Failed to canonicalize keymap path {:?}", initial_path)
-            })?;
+            let resolved_path = fs
+                .canonicalize(keymap_path)
+                .await
+                .with_context(|| format!("Failed to canonicalize keymap path {:?}", keymap_path))?;
             fs.atomic_write(resolved_path.clone(), new_text)
                 .await
                 .with_context(|| format!("Failed to write keymap to file {:?}", resolved_path))?;
         } else {
-            fs.atomic_write(initial_path.to_path_buf(), new_text)
+            fs.atomic_write(keymap_path.to_path_buf(), new_text)
                 .await
-                .with_context(|| format!("Failed to write keymap to file {:?}", initial_path))?;
+                .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?;
         }
 
         Ok(())

crates/settings/src/settings_store.rs 🔗

@@ -415,11 +415,11 @@ impl SettingsStore {
                     let new_text = cx.read_global(|store: &SettingsStore, cx| {
                         store.new_text_for_update::<T>(old_text, |content| update(content, cx))
                     })?;
-                    let initial_path = paths::settings_file().as_path();
-                    if fs.is_file(initial_path).await {
+                    let settings_path = paths::settings_file().as_path();
+                    if fs.is_file(settings_path).await {
                         let resolved_path =
-                            fs.canonicalize(initial_path).await.with_context(|| {
-                                format!("Failed to canonicalize settings path {:?}", initial_path)
+                            fs.canonicalize(settings_path).await.with_context(|| {
+                                format!("Failed to canonicalize settings path {:?}", settings_path)
                             })?;
 
                         fs.atomic_write(resolved_path.clone(), new_text)
@@ -428,10 +428,10 @@ impl SettingsStore {
                                 format!("Failed to write settings to file {:?}", resolved_path)
                             })?;
                     } else {
-                        fs.atomic_write(initial_path.to_path_buf(), new_text)
+                        fs.atomic_write(settings_path.to_path_buf(), new_text)
                             .await
                             .with_context(|| {
-                                format!("Failed to write settings to file {:?}", initial_path)
+                                format!("Failed to write settings to file {:?}", settings_path)
                             })?;
                     }
 
@@ -1011,17 +1011,16 @@ impl SettingsStore {
                     let Some(new_text) = migrate_settings(&old_text) else {
                         return anyhow::Ok(());
                     };
-                    let initial_path = paths::settings_file().as_path();
-                    if fs.is_file(initial_path).await {
-                        let backup_path = paths::home_dir().join(".zed_settings_backup");
-                        fs.atomic_write(backup_path, old_text)
+                    let settings_path = paths::settings_file().as_path();
+                    if fs.is_file(settings_path).await {
+                        fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text)
                             .await
                             .with_context(|| {
                                 "Failed to create settings backup in home directory".to_string()
                             })?;
                         let resolved_path =
-                            fs.canonicalize(initial_path).await.with_context(|| {
-                                format!("Failed to canonicalize settings path {:?}", initial_path)
+                            fs.canonicalize(settings_path).await.with_context(|| {
+                                format!("Failed to canonicalize settings path {:?}", settings_path)
                             })?;
                         fs.atomic_write(resolved_path.clone(), new_text)
                             .await
@@ -1029,10 +1028,10 @@ impl SettingsStore {
                                 format!("Failed to write settings to file {:?}", resolved_path)
                             })?;
                     } else {
-                        fs.atomic_write(initial_path.to_path_buf(), new_text)
+                        fs.atomic_write(settings_path.to_path_buf(), new_text)
                             .await
                             .with_context(|| {
-                                format!("Failed to write settings to file {:?}", initial_path)
+                                format!("Failed to write settings to file {:?}", settings_path)
                             })?;
                     }
                     anyhow::Ok(())

crates/workspace/src/notifications.rs 🔗

@@ -448,6 +448,14 @@ pub mod simple_message_notification {
             self
         }
 
+        pub fn primary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
+        where
+            F: 'static + Fn(&mut Window, &mut Context<Self>),
+        {
+            self.primary_on_click = Some(on_click);
+            self
+        }
+
         pub fn secondary_message<S>(mut self, message: S) -> Self
         where
             S: Into<SharedString>,
@@ -474,6 +482,14 @@ pub mod simple_message_notification {
             self
         }
 
+        pub fn secondary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
+        where
+            F: 'static + Fn(&mut Window, &mut Context<Self>),
+        {
+            self.secondary_on_click = Some(on_click);
+            self
+        }
+
         pub fn more_info_message<S>(mut self, message: S) -> Self
         where
             S: Into<SharedString>,

crates/zed/src/zed.rs 🔗

@@ -1217,25 +1217,29 @@ fn show_keymap_migration_notification_if_needed(
     if !KeymapFile::should_migrate_keymap(keymap_file) {
         return false;
     }
-    show_app_notification(notification_id, cx, move |cx| {
-        cx.new(move |_cx| {
-            let message = "A newer version of Zed has simplified several keymaps. Your existing keymaps may be deprecated. You can migrate them by clicking below. A backup will be created in your home directory.";
-            let button_text = "Backup and Migrate Keymap";
-            MessageNotification::new_from_builder(move |_, _| {
-                gpui::div().text_xs().child(message).into_any()
-            })
-            .primary_message(button_text)
-            .primary_on_click(move |_, cx| {
-                let fs = <dyn Fs>::global(cx);
-                cx.spawn(move |weak_notification, mut cx| async move {
-                    KeymapFile::migrate_keymap(fs).await.ok();
-                    weak_notification.update(&mut cx, |_, cx| {
+    let message = MarkdownString(format!(
+        "Keymap migration needed, as the format for some actions has changed. \
+        You can migrate your keymap by clicking below. A backup will be created at {}.",
+        MarkdownString::inline_code(&paths::keymap_backup_file().to_string_lossy())
+    ));
+    show_markdown_app_notification(
+        notification_id,
+        message,
+        "Backup and Migrate Keymap".into(),
+        move |_, cx| {
+            let fs = <dyn Fs>::global(cx);
+            cx.spawn(move |weak_notification, mut cx| async move {
+                KeymapFile::migrate_keymap(fs).await.ok();
+                weak_notification
+                    .update(&mut cx, |_, cx| {
                         cx.emit(DismissEvent);
-                    }).ok();
-                }).detach();
+                    })
+                    .ok();
             })
-        })
-    });
+            .detach();
+        },
+        cx,
+    );
     return true;
 }
 
@@ -1247,33 +1251,55 @@ fn show_settings_migration_notification_if_needed(
     if !SettingsStore::should_migrate_settings(&settings) {
         return;
     }
-    show_app_notification(notification_id, cx, move |cx| {
-        cx.new(move |_cx| {
-            let message = "A newer version of Zed has updated some settings. Your existing settings may be deprecated. You can migrate them by clicking below. A backup will be created in your home directory.";
-            let button_text = "Backup and Migrate Settings";
-            MessageNotification::new_from_builder(move |_, _| {
-                gpui::div().text_xs().child(message).into_any()
-            })
-            .primary_message(button_text)
-            .primary_on_click(move |_, cx| {
-                let fs = <dyn Fs>::global(cx);
-                cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs));
-                cx.emit(DismissEvent);
-            })
-        })
-    });
+    let message = MarkdownString(format!(
+        "Settings migration needed, as the format for some settings has changed. \
+            You can migrate your settings by clicking below. A backup will be created at {}.",
+        MarkdownString::inline_code(&paths::settings_backup_file().to_string_lossy())
+    ));
+    show_markdown_app_notification(
+        notification_id,
+        message,
+        "Backup and Migrate Settings".into(),
+        move |_, cx| {
+            let fs = <dyn Fs>::global(cx);
+            cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs));
+            cx.emit(DismissEvent);
+        },
+        cx,
+    );
 }
 
 fn show_keymap_file_load_error(
     notification_id: NotificationId,
-    markdown_error_message: MarkdownString,
+    error_message: MarkdownString,
     cx: &mut App,
 ) {
+    show_markdown_app_notification(
+        notification_id.clone(),
+        error_message,
+        "Open Keymap File".into(),
+        |window, cx| {
+            window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
+            cx.emit(DismissEvent);
+        },
+        cx,
+    )
+}
+
+fn show_markdown_app_notification<F>(
+    notification_id: NotificationId,
+    message: MarkdownString,
+    primary_button_message: SharedString,
+    primary_button_on_click: F,
+    cx: &mut App,
+) where
+    F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
+{
     let parsed_markdown = cx.background_executor().spawn(async move {
         let file_location_directory = None;
         let language_registry = None;
         markdown_preview::markdown_parser::parse_markdown(
-            &markdown_error_message.0,
+            &message.0,
             file_location_directory,
             language_registry,
         )
@@ -1282,10 +1308,14 @@ fn show_keymap_file_load_error(
 
     cx.spawn(move |cx| async move {
         let parsed_markdown = Arc::new(parsed_markdown.await);
+        let primary_button_message = primary_button_message.clone();
+        let primary_button_on_click = Arc::new(primary_button_on_click);
         cx.update(|cx| {
             show_app_notification(notification_id, cx, move |cx| {
                 let workspace_handle = cx.entity().downgrade();
                 let parsed_markdown = parsed_markdown.clone();
+                let primary_button_message = primary_button_message.clone();
+                let primary_button_on_click = primary_button_on_click.clone();
                 cx.new(move |_cx| {
                     MessageNotification::new_from_builder(move |window, cx| {
                         gpui::div()
@@ -1298,11 +1328,8 @@ fn show_keymap_file_load_error(
                             ))
                             .into_any()
                     })
-                    .primary_message("Open Keymap File")
-                    .primary_on_click(|window, cx| {
-                        window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
-                        cx.emit(DismissEvent);
-                    })
+                    .primary_message(primary_button_message)
+                    .primary_on_click_arc(primary_button_on_click)
                 })
             })
         })