Improve logic for finding VSCode / Cursor settings files (#32721)

* Fixes a bug where for Cursor, `config_dir()` (Zed's config dir) was
being used instead of `dirs::config_dir` (`~/.config` /
`$XDG_CONFIG_HOME`).

* Adds support for windows, before it was using the user profile folder
+ `/.config` which is incorrect.

* Now looks using a variety of product names - `["Code", "Code - OSS",
"Code Dev", "Code - OSS Dev", "code-oss-dev", "VSCodium"]`.

* Now shows settings path that was read before confirming import.

Including this path in the confirmation modal is a bit ugly (making it
link-styled and clickable would be nice), but I think it's better to
include it now that it is selecting the first match of a list of
candidate paths:


![image](https://github.com/user-attachments/assets/ceada4c2-96a6-4a84-a188-a1d93521ab26)

Release Notes:

- Added more settings file locations to check for VS Code / Cursor
settings import.
This commit is contained in:
Michael Sloan 2025-06-14 21:39:54 -06:00 committed by GitHub
parent afa70034d5
commit a5ceef35fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 138 additions and 59 deletions

2
Cargo.lock generated
View file

@ -14605,12 +14605,12 @@ dependencies = [
"fs",
"gpui",
"log",
"paths",
"schemars",
"serde",
"settings",
"theme",
"ui",
"util",
"workspace",
"workspace-hack",
]

View file

@ -1,5 +1,6 @@
//! Paths to locations used by Zed.
use std::env;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
@ -106,6 +107,7 @@ pub fn data_dir() -> &'static PathBuf {
}
})
}
/// Returns the path to the temp directory used by Zed.
pub fn temp_dir() -> &'static PathBuf {
static TEMP_DIR: OnceLock<PathBuf> = OnceLock::new();
@ -426,32 +428,74 @@ pub fn global_ssh_config_file() -> &'static Path {
Path::new("/etc/ssh/ssh_config")
}
/// Returns the path to the vscode user settings file
pub fn vscode_settings_file() -> &'static PathBuf {
static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();
let rel_path = "Code/User/settings.json";
LOGS_DIR.get_or_init(|| {
if cfg!(target_os = "macos") {
home_dir()
.join("Library/Application Support")
.join(rel_path)
} else {
home_dir().join(".config").join(rel_path)
}
})
/// Returns candidate paths for the vscode user settings file
pub fn vscode_settings_file_paths() -> Vec<PathBuf> {
let mut paths = vscode_user_data_paths();
for path in paths.iter_mut() {
path.push("User/settings.json");
}
paths
}
/// 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") {
/// Returns candidate paths for the cursor user settings file
pub fn cursor_settings_file_paths() -> Vec<PathBuf> {
let mut paths = cursor_user_data_paths();
for path in paths.iter_mut() {
path.push("User/settings.json");
}
paths
}
fn vscode_user_data_paths() -> Vec<PathBuf> {
// https://github.com/microsoft/vscode/blob/23e7148cdb6d8a27f0109ff77e5b1e019f8da051/src/vs/platform/environment/node/userDataPath.ts#L45
const VSCODE_PRODUCT_NAMES: &[&str] = &[
"Code",
"Code - OSS",
"VSCodium",
"Code Dev",
"Code - OSS Dev",
"code-oss-dev",
];
let mut paths = Vec::new();
if let Ok(portable_path) = env::var("VSCODE_PORTABLE") {
paths.push(Path::new(&portable_path).join("user-data"));
}
if let Ok(vscode_appdata) = env::var("VSCODE_APPDATA") {
for product_name in VSCODE_PRODUCT_NAMES {
paths.push(Path::new(&vscode_appdata).join(product_name));
}
}
for product_name in VSCODE_PRODUCT_NAMES {
add_vscode_user_data_paths(&mut paths, product_name);
}
paths
}
fn cursor_user_data_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
add_vscode_user_data_paths(&mut paths, "Cursor");
paths
}
fn add_vscode_user_data_paths(paths: &mut Vec<PathBuf>, product_name: &str) {
if cfg!(target_os = "macos") {
paths.push(
home_dir()
.join("Library/Application Support")
.join(rel_path)
} else {
config_dir().join(rel_path)
.join(product_name),
);
} else if cfg!(target_os = "windows") {
if let Some(data_local_dir) = dirs::data_local_dir() {
paths.push(data_local_dir.join(product_name));
}
})
if let Some(data_dir) = dirs::data_dir() {
paths.push(data_dir.join(product_name));
}
} else {
paths.push(
dirs::config_dir()
.unwrap_or(home_dir().join(".config"))
.join(product_name),
);
}
}

View file

@ -1,8 +1,8 @@
use anyhow::Result;
use anyhow::{Context as _, Result, anyhow};
use fs::Fs;
use paths::{cursor_settings_file_paths, vscode_settings_file_paths};
use serde_json::{Map, Value};
use std::sync::Arc;
use std::{path::Path, rc::Rc, sync::Arc};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum VsCodeSettingsSource {
@ -21,26 +21,59 @@ impl std::fmt::Display for VsCodeSettingsSource {
pub struct VsCodeSettings {
pub source: VsCodeSettingsSource,
pub path: Rc<Path>,
content: Map<String, Value>,
}
impl VsCodeSettings {
#[cfg(any(test, feature = "test-support"))]
pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
Ok(Self {
source,
path: Path::new("/example-path/Code/User/settings.json").into(),
content: serde_json_lenient::from_str(content)?,
})
}
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 candidate_paths = match source {
VsCodeSettingsSource::VsCode => vscode_settings_file_paths(),
VsCodeSettingsSource::Cursor => cursor_settings_file_paths(),
};
let content = fs.load(path).await?;
let mut path = None;
for candidate_path in candidate_paths.iter() {
if fs.is_file(candidate_path).await {
path = Some(candidate_path.clone());
}
}
let Some(path) = path else {
return Err(anyhow!(
"No settings file found, expected to find it in one of the following paths:\n{}",
candidate_paths
.into_iter()
.map(|path| path.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("\n")
));
};
let content = fs.load(&path).await.with_context(|| {
format!(
"Error loading {} settings file from {}",
source,
path.display()
)
})?;
let content = serde_json_lenient::from_str(&content).with_context(|| {
format!(
"Error parsing {} settings file from {}",
source,
path.display()
)
})?;
Ok(Self {
source,
content: serde_json_lenient::from_str(&content)?,
path: path.into(),
content,
})
}

View file

@ -18,11 +18,11 @@ feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
log.workspace = true
paths.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
workspace.workspace = true
util.workspace = true
workspace-hack.workspace = true
serde.workspace = true
schemars.workspace = true
workspace.workspace = true

View file

@ -15,6 +15,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use settings::{SettingsStore, VsCodeSettingsSource};
use ui::prelude::*;
use util::truncate_and_remove_front;
use workspace::item::{Item, ItemEvent};
use workspace::{Workspace, with_active_or_new_workspace};
@ -129,33 +130,32 @@ async fn handle_import_vscode_settings(
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 vscode_settings =
match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
Ok(vscode_settings) => vscode_settings,
Err(err) => {
log::error!("{err}");
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",
&format!(
"Importing {} settings may overwrite your existing settings. \
Will import settings from {}",
vscode_settings.source,
truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
),
None,
&["Ok", "Cancel"],
);
@ -166,9 +166,11 @@ async fn handle_import_vscode_settings(
}
cx.update(|_, cx| {
let source = vscode_settings.source;
let path = vscode_settings.path.clone();
cx.global::<SettingsStore>()
.import_vscode_settings(fs, vscode);
log::info!("Imported settings from {source}");
.import_vscode_settings(fs, vscode_settings);
log::info!("Imported {source} settings from {}", path.display());
})
.ok();
}