From 384868e597b046e27863dd28be55bcc3e3d16394 Mon Sep 17 00:00:00 2001 From: Marko Kungla Date: Fri, 11 Apr 2025 00:16:43 +0300 Subject: [PATCH] Add --user-data-dir CLI flag and propose renaming support_dir to data_dir (#26886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces support for a `--user-data-dir` CLI flag to override Zed's data directory and proposes renaming `support_dir` to `data_dir` for better cross-platform clarity. It builds on the discussion in #25349 about custom data directories, aiming to provide a flexible cross-platform solution. ### Changes The PR is split into two commits: 1. **[feat(cli): add --user-data-dir to override data directory](https://github.com/zed-industries/zed/pull/26886/commits/28e8889105847401e783d1739722d0998459fe5a)** 2. **[refactor(paths): rename support_dir to data_dir for cross-platform clarity](https://github.com/zed-industries/zed/pull/26886/commits/affd2fc606b39af1b25432a688a9006229a8fc3a)** ### Context Inspired by the need for custom data directories discussed in #25349, this PR provides an immediate implementation in the first commit, while the second commit suggests a naming improvement for broader appeal. @mikayla-maki, I’d appreciate your feedback, especially on the rename proposal, given your involvement in the original discussion! ### Testing - `cargo build ` - `./target/debug/zed --user-data-dir ~/custom-data-dir` Release Notes: - Added --user-data-dir CLI flag --------- Signed-off-by: Marko Kungla --- crates/agent/src/thread_store.rs | 2 +- crates/cli/src/cli.rs | 1 + crates/cli/src/main.rs | 70 +++++++-- crates/indexed_docs/src/providers/rustdoc.rs | 4 +- crates/node_runtime/src/node_runtime.rs | 4 +- crates/paths/src/paths.rs | 145 ++++++++++++------- crates/zed/src/main.rs | 13 ++ crates/zed/src/zed/open_listener.rs | 3 +- crates/zed/src/zed/windows_only_instance.rs | 1 + 9 files changed, 172 insertions(+), 71 deletions(-) diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 8a0140810e..aa7d515e68 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -491,7 +491,7 @@ impl ThreadsDatabase { let database_future = executor .spawn({ let executor = executor.clone(); - let database_path = paths::support_dir().join("threads/threads-db.1.mdb"); + let database_path = paths::data_dir().join("threads/threads-db.1.mdb"); async move { ThreadsDatabase::new(database_path, executor) } }) .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new))) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 9d23cf7ad5..32d32dba50 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -16,6 +16,7 @@ pub enum CliRequest { wait: bool, open_new_workspace: Option, env: Option>, + user_data_dir: Option, }, } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 17a578bd6c..72a06007a9 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -26,7 +26,11 @@ struct Detect; trait InstalledApp { fn zed_version_string(&self) -> String; fn launch(&self, ipc_url: String) -> anyhow::Result<()>; - fn run_foreground(&self, ipc_url: String) -> io::Result; + fn run_foreground( + &self, + ipc_url: String, + user_data_dir: Option<&str>, + ) -> io::Result; fn path(&self) -> PathBuf; } @@ -58,6 +62,13 @@ struct Args { /// Create a new workspace #[arg(short, long, overrides_with = "add")] new: bool, + /// Sets a custom directory for all user data (e.g., database, extensions, logs). + /// This overrides the default platform-specific data directory location. + /// On macOS, the default is `~/Library/Application Support/Zed`. + /// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`. + /// On Windows, the default is `%LOCALAPPDATA%\Zed`. + #[arg(long, value_name = "DIR")] + user_data_dir: Option, /// The paths to open in Zed (space-separated). /// /// Use `path:line:column` syntax to open a file at the given line and column. @@ -135,6 +146,12 @@ fn main() -> Result<()> { } let args = Args::parse(); + // Set custom data directory before any path operations + let user_data_dir = args.user_data_dir.clone(); + if let Some(dir) = &user_data_dir { + paths::set_custom_data_dir(dir); + } + #[cfg(any(target_os = "linux", target_os = "freebsd"))] let args = flatpak::set_bin_if_no_escape(args); @@ -246,6 +263,7 @@ fn main() -> Result<()> { let sender: JoinHandle> = thread::spawn({ let exit_status = exit_status.clone(); + let user_data_dir_for_thread = user_data_dir.clone(); move || { let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; let (tx, rx) = (handshake.requests, handshake.responses); @@ -256,6 +274,7 @@ fn main() -> Result<()> { wait: args.wait, open_new_workspace, env, + user_data_dir: user_data_dir_for_thread, })?; while let Ok(response) = rx.recv() { @@ -291,7 +310,7 @@ fn main() -> Result<()> { .collect(); if args.foreground { - app.run_foreground(url)?; + app.run_foreground(url, user_data_dir.as_deref())?; } else { app.launch(url)?; sender.join().unwrap()?; @@ -437,7 +456,7 @@ mod linux { } fn launch(&self, ipc_url: String) -> anyhow::Result<()> { - let sock_path = paths::support_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL)); + let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL)); let sock = UnixDatagram::unbound()?; if sock.connect(&sock_path).is_err() { self.boot_background(ipc_url)?; @@ -447,10 +466,17 @@ mod linux { Ok(()) } - fn run_foreground(&self, ipc_url: String) -> io::Result { - std::process::Command::new(self.0.clone()) - .arg(ipc_url) - .status() + fn run_foreground( + &self, + ipc_url: String, + user_data_dir: Option<&str>, + ) -> io::Result { + let mut cmd = std::process::Command::new(self.0.clone()); + cmd.arg(ipc_url); + if let Some(dir) = user_data_dir { + cmd.arg("--user-data-dir").arg(dir); + } + cmd.status() } fn path(&self) -> PathBuf { @@ -688,12 +714,17 @@ mod windows { Ok(()) } - fn run_foreground(&self, ipc_url: String) -> io::Result { - std::process::Command::new(self.0.clone()) - .arg(ipc_url) - .arg("--foreground") - .spawn()? - .wait() + fn run_foreground( + &self, + ipc_url: String, + user_data_dir: Option<&str>, + ) -> io::Result { + let mut cmd = std::process::Command::new(self.0.clone()); + cmd.arg(ipc_url).arg("--foreground"); + if let Some(dir) = user_data_dir { + cmd.arg("--user-data-dir").arg(dir); + } + cmd.spawn()?.wait() } fn path(&self) -> PathBuf { @@ -875,13 +906,22 @@ mod mac_os { Ok(()) } - fn run_foreground(&self, ipc_url: String) -> io::Result { + fn run_foreground( + &self, + ipc_url: String, + user_data_dir: Option<&str>, + ) -> io::Result { let path = match self { Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"), Bundle::LocalPath { executable, .. } => executable.clone(), }; - std::process::Command::new(path).arg(ipc_url).status() + let mut cmd = std::process::Command::new(path); + cmd.arg(ipc_url); + if let Some(dir) = user_data_dir { + cmd.arg("--user-data-dir").arg(dir); + } + cmd.status() } fn path(&self) -> PathBuf { diff --git a/crates/indexed_docs/src/providers/rustdoc.rs b/crates/indexed_docs/src/providers/rustdoc.rs index cda93fdc53..75c522c6ca 100644 --- a/crates/indexed_docs/src/providers/rustdoc.rs +++ b/crates/indexed_docs/src/providers/rustdoc.rs @@ -53,7 +53,7 @@ impl IndexedDocsProvider for LocalRustdocProvider { } fn database_path(&self) -> PathBuf { - paths::support_dir().join("docs/rust/rustdoc-db.1.mdb") + paths::data_dir().join("docs/rust/rustdoc-db.1.mdb") } async fn suggest_packages(&self) -> Result> { @@ -144,7 +144,7 @@ impl IndexedDocsProvider for DocsDotRsProvider { } fn database_path(&self) -> PathBuf { - paths::support_dir().join("docs/rust/docs-rs-db.1.mdb") + paths::data_dir().join("docs/rust/docs-rs-db.1.mdb") } async fn suggest_packages(&self) -> Result> { diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index c3667a93c7..ae0260799e 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -312,7 +312,7 @@ impl ManagedNodeRuntime { let version = Self::VERSION; let folder_name = format!("node-{version}-{os}-{arch}"); - let node_containing_dir = paths::support_dir().join("node"); + let node_containing_dir = paths::data_dir().join("node"); let node_dir = node_containing_dir.join(folder_name); let node_binary = node_dir.join(Self::NODE_PATH); let npm_file = node_dir.join(Self::NPM_PATH); @@ -498,7 +498,7 @@ impl SystemNodeRuntime { ) } - let scratch_dir = paths::support_dir().join("node"); + let scratch_dir = paths::data_dir().join("node"); fs::create_dir(&scratch_dir).await.ok(); fs::create_dir(scratch_dir.join("cache")).await.ok(); diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 73dff4d259..622e4a67f3 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -5,61 +5,109 @@ use std::sync::OnceLock; pub use util::paths::home_dir; +/// A default editorconfig file name to use when resolving project settings. +pub const EDITORCONFIG_NAME: &str = ".editorconfig"; + +/// A custom data directory override, set only by `set_custom_data_dir`. +/// This is used to override the default data directory location. +/// The directory will be created if it doesn't exist when set. +static CUSTOM_DATA_DIR: OnceLock = OnceLock::new(); + +/// The resolved data directory, combining custom override or platform defaults. +/// This is set once and cached for subsequent calls. +/// On macOS, this is `~/Library/Application Support/Zed`. +/// On Linux/FreeBSD, this is `$XDG_DATA_HOME/zed`. +/// On Windows, this is `%LOCALAPPDATA%\Zed`. +static CURRENT_DATA_DIR: OnceLock = OnceLock::new(); + +/// The resolved config directory, combining custom override or platform defaults. +/// This is set once and cached for subsequent calls. +/// On macOS, this is `~/.config/zed`. +/// On Linux/FreeBSD, this is `$XDG_CONFIG_HOME/zed`. +/// On Windows, this is `%APPDATA%\Zed`. +static CONFIG_DIR: OnceLock = OnceLock::new(); + /// Returns the relative path to the zed_server directory on the ssh host. pub fn remote_server_dir_relative() -> &'static Path { Path::new(".zed_server") } +/// Sets a custom directory for all user data, overriding the default data directory. +/// This function must be called before any other path operations that depend on the data directory. +/// The directory will be created if it doesn't exist. +/// +/// # Arguments +/// +/// * `dir` - The path to use as the custom data directory. This will be used as the base +/// directory for all user data, including databases, extensions, and logs. +/// +/// # Returns +/// +/// A reference to the static `PathBuf` containing the custom data directory path. +/// +/// # Panics +/// +/// Panics if: +/// * Called after the data directory has been initialized (e.g., via `data_dir` or `config_dir`) +/// * The directory cannot be created +pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf { + if CURRENT_DATA_DIR.get().is_some() || CONFIG_DIR.get().is_some() { + panic!("set_custom_data_dir called after data_dir or config_dir was initialized"); + } + CUSTOM_DATA_DIR.get_or_init(|| { + let path = PathBuf::from(dir); + std::fs::create_dir_all(&path).expect("failed to create custom data directory"); + path + }) +} + /// Returns the path to the configuration directory used by Zed. pub fn config_dir() -> &'static PathBuf { - static CONFIG_DIR: OnceLock = OnceLock::new(); CONFIG_DIR.get_or_init(|| { - if cfg!(target_os = "windows") { - return dirs::config_dir() + if let Some(custom_dir) = CUSTOM_DATA_DIR.get() { + custom_dir.join("config") + } else if cfg!(target_os = "windows") { + dirs::config_dir() .expect("failed to determine RoamingAppData directory") - .join("Zed"); - } - - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - return if let Ok(flatpak_xdg_config) = std::env::var("FLATPAK_XDG_CONFIG_HOME") { + .join("Zed") + } else if cfg!(any(target_os = "linux", target_os = "freebsd")) { + if let Ok(flatpak_xdg_config) = std::env::var("FLATPAK_XDG_CONFIG_HOME") { flatpak_xdg_config.into() } else { - dirs::config_dir().expect("failed to determine XDG_CONFIG_HOME directory") + dirs::config_dir() + .expect("failed to determine XDG_CONFIG_HOME directory") + .join("zed") } - .join("zed"); + } else { + home_dir().join(".config").join("zed") } - - home_dir().join(".config").join("zed") }) } -/// Returns the path to the support directory used by Zed. -pub fn support_dir() -> &'static PathBuf { - static SUPPORT_DIR: OnceLock = OnceLock::new(); - SUPPORT_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - return home_dir().join("Library/Application Support/Zed"); - } - - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - return if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") { +/// Returns the path to the data directory used by Zed. +pub fn data_dir() -> &'static PathBuf { + CURRENT_DATA_DIR.get_or_init(|| { + if let Some(custom_dir) = CUSTOM_DATA_DIR.get() { + custom_dir.clone() + } else if cfg!(target_os = "macos") { + home_dir().join("Library/Application Support/Zed") + } else if cfg!(any(target_os = "linux", target_os = "freebsd")) { + if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") { flatpak_xdg_data.into() } else { - dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory") + dirs::data_local_dir() + .expect("failed to determine XDG_DATA_HOME directory") + .join("zed") } - .join("zed"); - } - - if cfg!(target_os = "windows") { - return dirs::data_local_dir() + } else if cfg!(target_os = "windows") { + dirs::data_local_dir() .expect("failed to determine LocalAppData directory") - .join("Zed"); + .join("Zed") + } else { + config_dir().clone() // Fallback } - - config_dir().clone() }) } - /// Returns the path to the temp directory used by Zed. pub fn temp_dir() -> &'static PathBuf { static TEMP_DIR: OnceLock = OnceLock::new(); @@ -96,7 +144,7 @@ pub fn logs_dir() -> &'static PathBuf { if cfg!(target_os = "macos") { home_dir().join("Library/Logs/Zed") } else { - support_dir().join("logs") + data_dir().join("logs") } }) } @@ -104,7 +152,7 @@ pub fn logs_dir() -> &'static PathBuf { /// Returns the path to the Zed server directory on this SSH host. pub fn remote_server_state_dir() -> &'static PathBuf { static REMOTE_SERVER_STATE: OnceLock = OnceLock::new(); - REMOTE_SERVER_STATE.get_or_init(|| support_dir().join("server_state")) + REMOTE_SERVER_STATE.get_or_init(|| data_dir().join("server_state")) } /// Returns the path to the `Zed.log` file. @@ -122,7 +170,7 @@ pub fn old_log_file() -> &'static PathBuf { /// Returns the path to the database directory. pub fn database_dir() -> &'static PathBuf { static DATABASE_DIR: OnceLock = OnceLock::new(); - DATABASE_DIR.get_or_init(|| support_dir().join("db")) + DATABASE_DIR.get_or_init(|| data_dir().join("db")) } /// Returns the path to the crashes directory, if it exists for the current platform. @@ -180,7 +228,7 @@ pub fn debug_tasks_file() -> &'static PathBuf { /// This is where installed extensions are stored. pub fn extensions_dir() -> &'static PathBuf { static EXTENSIONS_DIR: OnceLock = OnceLock::new(); - EXTENSIONS_DIR.get_or_init(|| support_dir().join("extensions")) + EXTENSIONS_DIR.get_or_init(|| data_dir().join("extensions")) } /// Returns the path to the extensions directory. @@ -188,7 +236,7 @@ pub fn extensions_dir() -> &'static PathBuf { /// This is where installed extensions are stored on a remote. pub fn remote_extensions_dir() -> &'static PathBuf { static EXTENSIONS_DIR: OnceLock = OnceLock::new(); - EXTENSIONS_DIR.get_or_init(|| support_dir().join("remote_extensions")) + EXTENSIONS_DIR.get_or_init(|| data_dir().join("remote_extensions")) } /// Returns the path to the extensions directory. @@ -222,7 +270,7 @@ pub fn contexts_dir() -> &'static PathBuf { if cfg!(target_os = "macos") { config_dir().join("conversations") } else { - support_dir().join("conversations") + data_dir().join("conversations") } }) } @@ -236,7 +284,7 @@ pub fn prompts_dir() -> &'static PathBuf { if cfg!(target_os = "macos") { config_dir().join("prompts") } else { - support_dir().join("prompts") + data_dir().join("prompts") } }) } @@ -262,7 +310,7 @@ pub fn prompt_overrides_dir(repo_path: Option<&Path>) -> PathBuf { if cfg!(target_os = "macos") { config_dir().join("prompt_overrides") } else { - support_dir().join("prompt_overrides") + data_dir().join("prompt_overrides") } }) .clone() @@ -277,7 +325,7 @@ pub fn embeddings_dir() -> &'static PathBuf { if cfg!(target_os = "macos") { config_dir().join("embeddings") } else { - support_dir().join("embeddings") + data_dir().join("embeddings") } }) } @@ -287,7 +335,7 @@ pub fn embeddings_dir() -> &'static PathBuf { /// This is where language servers are downloaded to for languages built-in to Zed. pub fn languages_dir() -> &'static PathBuf { static LANGUAGES_DIR: OnceLock = OnceLock::new(); - LANGUAGES_DIR.get_or_init(|| support_dir().join("languages")) + LANGUAGES_DIR.get_or_init(|| data_dir().join("languages")) } /// Returns the path to the debug adapters directory @@ -295,31 +343,31 @@ pub fn languages_dir() -> &'static PathBuf { /// This is where debug adapters are downloaded to for DAPs that are built-in to Zed. pub fn debug_adapters_dir() -> &'static PathBuf { static DEBUG_ADAPTERS_DIR: OnceLock = OnceLock::new(); - DEBUG_ADAPTERS_DIR.get_or_init(|| support_dir().join("debug_adapters")) + DEBUG_ADAPTERS_DIR.get_or_init(|| data_dir().join("debug_adapters")) } /// Returns the path to the Copilot directory. pub fn copilot_dir() -> &'static PathBuf { static COPILOT_DIR: OnceLock = OnceLock::new(); - COPILOT_DIR.get_or_init(|| support_dir().join("copilot")) + COPILOT_DIR.get_or_init(|| data_dir().join("copilot")) } /// Returns the path to the Supermaven directory. pub fn supermaven_dir() -> &'static PathBuf { static SUPERMAVEN_DIR: OnceLock = OnceLock::new(); - SUPERMAVEN_DIR.get_or_init(|| support_dir().join("supermaven")) + SUPERMAVEN_DIR.get_or_init(|| data_dir().join("supermaven")) } /// Returns the path to the default Prettier directory. pub fn default_prettier_dir() -> &'static PathBuf { static DEFAULT_PRETTIER_DIR: OnceLock = OnceLock::new(); - DEFAULT_PRETTIER_DIR.get_or_init(|| support_dir().join("prettier")) + DEFAULT_PRETTIER_DIR.get_or_init(|| data_dir().join("prettier")) } /// Returns the path to the remote server binaries directory. pub fn remote_servers_dir() -> &'static PathBuf { static REMOTE_SERVERS_DIR: OnceLock = OnceLock::new(); - REMOTE_SERVERS_DIR.get_or_init(|| support_dir().join("remote_servers")) + REMOTE_SERVERS_DIR.get_or_init(|| data_dir().join("remote_servers")) } /// Returns the relative path to a `.zed` folder within a project. @@ -359,6 +407,3 @@ pub fn local_debug_file_relative_path() -> &'static Path { pub fn local_vscode_launch_file_relative_path() -> &'static Path { Path::new(".vscode/launch.json") } - -/// A default editorconfig file name to use when resolving project settings. -pub const EDITORCONFIG_NAME: &str = ".editorconfig"; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0f34b54777..b18a8a7c62 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -172,6 +172,11 @@ fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) { fn main() { let args = Args::parse(); + // Set custom data directory. + if let Some(dir) = &args.user_data_dir { + paths::set_custom_data_dir(dir); + } + #[cfg(all(not(debug_assertions), target_os = "windows"))] unsafe { use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole}; @@ -962,6 +967,14 @@ struct Args { /// URLs can either be `file://` or `zed://` scheme, or relative to . paths_or_urls: Vec, + /// Sets a custom directory for all user data (e.g., database, extensions, logs). + /// This overrides the default platform-specific data directory location. + /// On macOS, the default is `~/Library/Application Support/Zed`. + /// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`. + /// On Windows, the default is `%LOCALAPPDATA%\Zed`. + #[arg(long, value_name = "DIR")] + user_data_dir: Option, + /// Instructs zed to run as a dev server on this machine. (not implemented) #[arg(long)] dev_server_token: Option, diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 03e3778b7a..3d033beffc 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -151,7 +151,7 @@ pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> { use release_channel::RELEASE_CHANNEL_NAME; use std::os::unix::net::UnixDatagram; - let sock_path = paths::support_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL_NAME)); + let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL_NAME)); // remove the socket if the process listening on it has died if let Err(e) = UnixDatagram::unbound()?.connect(&sock_path) { if e.kind() == std::io::ErrorKind::ConnectionRefused { @@ -261,6 +261,7 @@ pub async fn handle_cli_connection( wait, open_new_workspace, env, + user_data_dir: _, // Ignore user_data_dir } => { if !urls.is_empty() { cx.update(|cx| { diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index e3d20f6527..92295b5006 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -130,6 +130,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { wait: false, open_new_workspace: None, env: None, + user_data_dir: args.user_data_dir.clone(), } };