diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index d7a237cf49..f7e60ba203 100644 --- a/crates/language/src/language_settings.rs +++ b/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()), + ); + } } } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index c96d114ac1..4fe429da2e 100644 --- a/crates/paths/src/paths.rs +++ b/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 = 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) + } + }) +} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 8db108778f..89411ff2ce 100644 --- a/crates/settings/src/settings.rs +++ b/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); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 80365cab0d..e6e2a448e0 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1522,6 +1522,8 @@ pub fn parse_json_with_comments(content: &str) -> Result #[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); } diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 08a6b3e8d3..a3997820a4 100644 --- a/crates/settings/src/vscode_import.rs +++ b/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, } impl VsCodeSettings { - pub fn from_str(content: &str) -> Result { + pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result { Ok(Self { + source, content: serde_json_lenient::from_str(content)?, }) } - pub async fn load_user_settings(fs: Arc) -> Result { - let content = fs.load(paths::vscode_settings_file()).await?; + pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc) -> Result { + 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)?, }) } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 3428a99bf8..58d0ce9147 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/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() - )) - ); + handle_import_vscode_settings( + VsCodeSettingsSource::VsCode, + action.skip_prompt, + fs, + cx, + ) + .await + }) + .detach(); + }); - let _ = cx.prompt( - gpui::PromptLevel::Info, - "Could not find or load a VsCode settings file", - None, - &["Ok"], - ); - return; - } - }; + workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| { + let fs = ::global(cx); + let action = *action; - 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::() - .import_vscode_settings(fs, vscode); - log::info!("Imported settings from VsCode"); - }) - .ok(); + 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, + 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::() + .import_vscode_settings(fs, vscode); + log::info!("Imported settings from {source}"); + }) + .ok(); +} + pub struct SettingsPage { focus_handle: FocusHandle, }