Cursor settings import (#31424)

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
This commit is contained in:
Ben Kunkle 2025-05-27 13:14:25 -05:00 committed by GitHub
parent 21bd91a773
commit b9a5d437db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 147 additions and 48 deletions

View file

@ -1493,8 +1493,27 @@ impl settings::Settings for AllLanguageSettings {
associations.entry(v.into()).or_default().push(k.clone()); 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 // TODO: do we want to merge imported globs per filetype? for now we'll just replace
current.file_types.extend(associations); 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()),
);
}
} }
} }

View file

@ -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)
}
})
}

View file

@ -22,7 +22,7 @@ pub use settings_store::{
InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
SettingsStore, parse_json_with_comments, 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)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize); pub struct WorktreeId(usize);

View file

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

View file

@ -4,20 +4,42 @@ use serde_json::{Map, Value};
use std::sync::Arc; 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 struct VsCodeSettings {
pub source: VsCodeSettingsSource,
content: Map<String, Value>, content: Map<String, Value>,
} }
impl VsCodeSettings { impl VsCodeSettings {
pub fn from_str(content: &str) -> Result<Self> { pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
Ok(Self { Ok(Self {
source,
content: serde_json_lenient::from_str(content)?, content: serde_json_lenient::from_str(content)?,
}) })
} }
pub async fn load_user_settings(fs: Arc<dyn Fs>) -> Result<Self> { pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> {
let content = fs.load(paths::vscode_settings_file()).await?; let path = match source {
VsCodeSettingsSource::VsCode => paths::vscode_settings_file(),
VsCodeSettingsSource::Cursor => paths::cursor_settings_file(),
};
let content = fs.load(path).await?;
Ok(Self { Ok(Self {
source,
content: serde_json_lenient::from_str(&content)?, content: serde_json_lenient::from_str(&content)?,
}) })
} }

View file

@ -1,6 +1,7 @@
mod appearance_settings_controls; mod appearance_settings_controls;
use std::any::TypeId; use std::any::TypeId;
use std::sync::Arc;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use editor::EditorSettingsControls; use editor::EditorSettingsControls;
@ -12,7 +13,7 @@ use gpui::{
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use settings::SettingsStore; use settings::{SettingsStore, VsCodeSettingsSource};
use ui::prelude::*; use ui::prelude::*;
use workspace::Workspace; use workspace::Workspace;
use workspace::item::{Item, ItemEvent}; use workspace::item::{Item, ItemEvent};
@ -31,7 +32,13 @@ pub struct ImportVsCodeSettings {
pub skip_prompt: bool, 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]); actions!(zed, [OpenSettingsEditor]);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
@ -61,49 +68,30 @@ pub fn init(cx: &mut App) {
window window
.spawn(cx, async move |cx: &mut AsyncWindowContext| { .spawn(cx, async move |cx: &mut AsyncWindowContext| {
let vscode = handle_import_vscode_settings(
match settings::VsCodeSettings::load_user_settings(fs.clone()).await { VsCodeSettingsSource::VsCode,
Ok(vscode) => vscode, action.skip_prompt,
Err(err) => { fs,
println!( cx,
"Failed to load VsCode settings: {}", )
err.context(format!( .await
"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(); .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(); .detach();
}); });
@ -133,6 +121,56 @@ pub fn init(cx: &mut App) {
.detach(); .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 { pub struct SettingsPage {
focus_handle: FocusHandle, focus_handle: FocusHandle,
} }