Cursor settings import (#31424)

Ben Kunkle created

Closes #ISSUE

Release Notes:

- Added support for importing settings from cursor. Cursor settings can
be imported using the `zed: import cursor settings` command from the
command palette

Change summary

crates/language/src/language_settings.rs |  19 +++
crates/paths/src/paths.rs                |  15 +++
crates/settings/src/settings.rs          |   2 
crates/settings/src/settings_store.rs    |   7 +
crates/settings/src/vscode_import.rs     |  28 +++++
crates/settings_ui/src/settings_ui.rs    | 128 ++++++++++++++++---------
6 files changed, 149 insertions(+), 50 deletions(-)

Detailed changes

crates/language/src/language_settings.rs 🔗

@@ -1493,8 +1493,27 @@ impl settings::Settings for AllLanguageSettings {
                 associations.entry(v.into()).or_default().push(k.clone());
             }
         }
+
         // TODO: do we want to merge imported globs per filetype? for now we'll just replace
         current.file_types.extend(associations);
+
+        // cursor global ignore list applies to cursor-tab, so transfer it to edit_predictions.disabled_globs
+        if let Some(disabled_globs) = vscode
+            .read_value("cursor.general.globalCursorIgnoreList")
+            .and_then(|v| v.as_array())
+        {
+            current
+                .edit_predictions
+                .get_or_insert_default()
+                .disabled_globs
+                .get_or_insert_default()
+                .extend(
+                    disabled_globs
+                        .iter()
+                        .filter_map(|glob| glob.as_str())
+                        .map(|s| s.to_string()),
+                );
+        }
     }
 }
 

crates/paths/src/paths.rs 🔗

@@ -439,3 +439,18 @@ pub fn vscode_settings_file() -> &'static PathBuf {
         }
     })
 }
+
+/// Returns the path to the cursor user settings file
+pub fn cursor_settings_file() -> &'static PathBuf {
+    static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();
+    let rel_path = "Cursor/User/settings.json";
+    LOGS_DIR.get_or_init(|| {
+        if cfg!(target_os = "macos") {
+            home_dir()
+                .join("Library/Application Support")
+                .join(rel_path)
+        } else {
+            config_dir().join(rel_path)
+        }
+    })
+}

crates/settings/src/settings.rs 🔗

@@ -22,7 +22,7 @@ pub use settings_store::{
     InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
     SettingsStore, parse_json_with_comments,
 };
-pub use vscode_import::VsCodeSettings;
+pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
 pub struct WorktreeId(usize);

crates/settings/src/settings_store.rs 🔗

@@ -1522,6 +1522,8 @@ pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T>
 
 #[cfg(test)]
 mod tests {
+    use crate::VsCodeSettingsSource;
+
     use super::*;
     use serde_derive::Deserialize;
     use unindent::Unindent;
@@ -2004,7 +2006,10 @@ mod tests {
         cx: &mut App,
     ) {
         store.set_user_settings(&old, cx).ok();
-        let new = store.get_vscode_edits(old, &VsCodeSettings::from_str(&vscode).unwrap());
+        let new = store.get_vscode_edits(
+            old,
+            &VsCodeSettings::from_str(&vscode, VsCodeSettingsSource::VsCode).unwrap(),
+        );
         pretty_assertions::assert_eq!(new, expected);
     }
 

crates/settings/src/vscode_import.rs 🔗

@@ -4,20 +4,42 @@ use serde_json::{Map, Value};
 
 use std::sync::Arc;
 
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum VsCodeSettingsSource {
+    VsCode,
+    Cursor,
+}
+
+impl std::fmt::Display for VsCodeSettingsSource {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            VsCodeSettingsSource::VsCode => write!(f, "VS Code"),
+            VsCodeSettingsSource::Cursor => write!(f, "Cursor"),
+        }
+    }
+}
+
 pub struct VsCodeSettings {
+    pub source: VsCodeSettingsSource,
     content: Map<String, Value>,
 }
 
 impl VsCodeSettings {
-    pub fn from_str(content: &str) -> Result<Self> {
+    pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
         Ok(Self {
+            source,
             content: serde_json_lenient::from_str(content)?,
         })
     }
 
-    pub async fn load_user_settings(fs: Arc<dyn Fs>) -> Result<Self> {
-        let content = fs.load(paths::vscode_settings_file()).await?;
+    pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> {
+        let path = match source {
+            VsCodeSettingsSource::VsCode => paths::vscode_settings_file(),
+            VsCodeSettingsSource::Cursor => paths::cursor_settings_file(),
+        };
+        let content = fs.load(path).await?;
         Ok(Self {
+            source,
             content: serde_json_lenient::from_str(&content)?,
         })
     }

crates/settings_ui/src/settings_ui.rs 🔗

@@ -1,6 +1,7 @@
 mod appearance_settings_controls;
 
 use std::any::TypeId;
+use std::sync::Arc;
 
 use command_palette_hooks::CommandPaletteFilter;
 use editor::EditorSettingsControls;
@@ -12,7 +13,7 @@ use gpui::{
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
-use settings::SettingsStore;
+use settings::{SettingsStore, VsCodeSettingsSource};
 use ui::prelude::*;
 use workspace::Workspace;
 use workspace::item::{Item, ItemEvent};
@@ -31,7 +32,13 @@ pub struct ImportVsCodeSettings {
     pub skip_prompt: bool,
 }
 
-impl_actions!(zed, [ImportVsCodeSettings]);
+#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema)]
+pub struct ImportCursorSettings {
+    #[serde(default)]
+    pub skip_prompt: bool,
+}
+
+impl_actions!(zed, [ImportVsCodeSettings, ImportCursorSettings]);
 actions!(zed, [OpenSettingsEditor]);
 
 pub fn init(cx: &mut App) {
@@ -61,49 +68,30 @@ pub fn init(cx: &mut App) {
 
             window
                 .spawn(cx, async move |cx: &mut AsyncWindowContext| {
-                    let vscode =
-                        match settings::VsCodeSettings::load_user_settings(fs.clone()).await {
-                            Ok(vscode) => vscode,
-                            Err(err) => {
-                                println!(
-                                    "Failed to load VsCode settings: {}",
-                                    err.context(format!(
-                                        "Loading VsCode settings from path: {:?}",
-                                        paths::vscode_settings_file()
-                                    ))
-                                );
-
-                                let _ = cx.prompt(
-                                    gpui::PromptLevel::Info,
-                                    "Could not find or load a VsCode settings file",
-                                    None,
-                                    &["Ok"],
-                                );
-                                return;
-                            }
-                        };
-
-                    let prompt = if action.skip_prompt {
-                        Task::ready(Some(0))
-                    } else {
-                        let prompt = cx.prompt(
-                            gpui::PromptLevel::Warning,
-                            "Importing settings may overwrite your existing settings",
-                            None,
-                            &["Ok", "Cancel"],
-                        );
-                        cx.spawn(async move |_| prompt.await.ok())
-                    };
-                    if prompt.await != Some(0) {
-                        return;
-                    }
-
-                    cx.update(|_, cx| {
-                        cx.global::<SettingsStore>()
-                            .import_vscode_settings(fs, vscode);
-                        log::info!("Imported settings from VsCode");
-                    })
-                    .ok();
+                    handle_import_vscode_settings(
+                        VsCodeSettingsSource::VsCode,
+                        action.skip_prompt,
+                        fs,
+                        cx,
+                    )
+                    .await
+                })
+                .detach();
+        });
+
+        workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
+            let fs = <dyn Fs>::global(cx);
+            let action = *action;
+
+            window
+                .spawn(cx, async move |cx: &mut AsyncWindowContext| {
+                    handle_import_vscode_settings(
+                        VsCodeSettingsSource::Cursor,
+                        action.skip_prompt,
+                        fs,
+                        cx,
+                    )
+                    .await
                 })
                 .detach();
         });
@@ -133,6 +121,56 @@ pub fn init(cx: &mut App) {
     .detach();
 }
 
+async fn handle_import_vscode_settings(
+    source: VsCodeSettingsSource,
+    skip_prompt: bool,
+    fs: Arc<dyn Fs>,
+    cx: &mut AsyncWindowContext,
+) {
+    let vscode = match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
+        Ok(vscode) => vscode,
+        Err(err) => {
+            println!(
+                "Failed to load {source} settings: {}",
+                err.context(format!(
+                    "Loading {source} settings from path: {:?}",
+                    paths::vscode_settings_file()
+                ))
+            );
+
+            let _ = cx.prompt(
+                gpui::PromptLevel::Info,
+                &format!("Could not find or load a {source} settings file"),
+                None,
+                &["Ok"],
+            );
+            return;
+        }
+    };
+
+    let prompt = if skip_prompt {
+        Task::ready(Some(0))
+    } else {
+        let prompt = cx.prompt(
+            gpui::PromptLevel::Warning,
+            "Importing settings may overwrite your existing settings",
+            None,
+            &["Ok", "Cancel"],
+        );
+        cx.spawn(async move |_| prompt.await.ok())
+    };
+    if prompt.await != Some(0) {
+        return;
+    }
+
+    cx.update(|_, cx| {
+        cx.global::<SettingsStore>()
+            .import_vscode_settings(fs, vscode);
+        log::info!("Imported settings from {source}");
+    })
+    .ok();
+}
+
 pub struct SettingsPage {
     focus_handle: FocusHandle,
 }