From a5ceef35fa478cd2f0e14277acd9babdb520a9d5 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Sat, 14 Jun 2025 21:39:54 -0600 Subject: [PATCH] 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. --- Cargo.lock | 2 +- crates/paths/src/paths.rs | 90 ++++++++++++++++++++------- crates/settings/src/vscode_import.rs | 49 ++++++++++++--- crates/settings_ui/Cargo.toml | 8 +-- crates/settings_ui/src/settings_ui.rs | 48 +++++++------- 5 files changed, 138 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e64097c15c..7c287c0f9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14605,12 +14605,12 @@ dependencies = [ "fs", "gpui", "log", - "paths", "schemars", "serde", "settings", "theme", "ui", + "util", "workspace", "workspace-hack", ] diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 088189f814..f447e474e7 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -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 = 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 = 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 { + 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 = 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 { + 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 { + // 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 { + let mut paths = Vec::new(); + add_vscode_user_data_paths(&mut paths, "Cursor"); + paths +} + +fn add_vscode_user_data_paths(paths: &mut Vec, 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), + ); + } } diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index a3997820a4..4a48c18f7c 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -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, content: Map, } impl VsCodeSettings { + #[cfg(any(test, feature = "test-support"))] pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result { 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) -> Result { - 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::>() + .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, }) } diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 9eacacb9e0..84d77e3fdc 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -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 diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index b7bb4b77e7..da57845c61 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -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, 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::() - .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(); }