#![allow( clippy::disallowed_methods, reason = "We are not in an async environment, so std::process::Command is fine")]#![cfg_attr( any(target_os = "linux", target_os = "freebsd", target_os = "windows"), allow(dead_code))] use anyhow::{Context as _, Result};use clap::Parser;use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};use parking_lot::Mutex;use std::{ env, ffi::OsStr, fs, io, path::{Path, PathBuf}, process::ExitStatus, sync::Arc, thread::{self, JoinHandle},};use tempfile::NamedTempFile;use util::paths::PathWithPosition; #[cfg(any(target_os = "linux", target_os = "freebsd"))]use std::io::IsTerminal; const URL_PREFIX: [&'static str; 5] = ["zed://", "http://", "https://", "file://", "ssh://"]; struct Detect; trait InstalledApp { fn zed_version_string(&self) -> String; fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()>; fn run_foreground( &self, ipc_url: String, user_data_dir: Option<&str>, ) -> io::Result<ExitStatus>; fn path(&self) -> PathBuf;} #[derive(Parser, Debug)]#[command( name = "zed", disable_version_flag = true, before_help = "The Zed CLI binary.This CLI is a separate binary that invokes Zed. Examples: `zed` Simply opens Zed `zed --foreground` Runs in foreground (shows all logs) `zed path-to-your-project` Open your project in Zed `zed -n path-to-file ` Open file/folder in a new window", after_help = "To read from stdin, append '-', e.g. 'ps axf | zed -'")]struct Args { /// Wait for all of the given paths to be opened/closed before exiting. /// /// When opening a directory, waits until the created window is closed. #[arg(short, long)] wait: bool, /// Add files to the currently open workspace #[arg(short, long, overrides_with_all = ["new", "reuse"])] add: bool, /// Create a new workspace #[arg(short, long, overrides_with_all = ["add", "reuse"])] new: bool, /// Reuse an existing window, replacing its workspace #[arg(short, long, overrides_with_all = ["add", "new"])] reuse: bool, /// Sets a custom directory for all user data (e.g., database, extensions, logs). /// This overrides the default platform-specific data directory location: #[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")] #[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\Zed`.")] #[cfg_attr( not(any(target_os = "windows", target_os = "macos")), doc = "`$XDG_DATA_HOME/zed`." )] #[arg(long, value_name = "DIR")] user_data_dir: Option<String>, /// The paths to open in Zed (space-separated). /// /// Use `path:line:column` syntax to open a file at the given line and column. paths_with_position: Vec<String>, /// Print Zed's version and the app path. #[arg(short, long)] version: bool, /// Run zed in the foreground (useful for debugging) #[arg(long)] foreground: bool, /// Custom path to Zed.app or the zed binary #[arg(long)] zed: Option<PathBuf>, /// Run zed in dev-server mode #[arg(long)] dev_server_token: Option<String>, /// The username and WSL distribution to use when opening paths. If not specified, /// Zed will attempt to open the paths directly. /// /// The username is optional, and if not specified, the default user for the distribution /// will be used. /// /// Example: `me@Ubuntu` or `Ubuntu`. /// /// WARN: You should not fill in this field by hand. #[cfg(target_os = "windows")] #[arg(long, value_name = "USER@DISTRO")] wsl: Option<String>, /// Not supported in Zed CLI, only supported on Zed binary /// Will attempt to give the correct command to run #[arg(long)] system_specs: bool, /// Pairs of file paths to diff. Can be specified multiple times. #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] diff: Vec<String>, /// Uninstall Zed from user system #[cfg(all( any(target_os = "linux", target_os = "macos"), not(feature = "no-bundled-uninstall") ))] #[arg(long)] uninstall: bool, /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency, /// by having Zed act like netcat communicating over a Unix socket. #[arg(long, hide = true)] askpass: Option<String>,} /// Parses a path containing a position (e.g. `path:line:column`)/// and returns its canonicalized string representation.////// If a part of path doesn't exist, it will canonicalize the/// existing part and append the non-existing part.////// This method must return an absolute path, as many zed/// crates assume absolute paths.fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> { match Path::new(argument_str).canonicalize() { Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)), Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| { let curdir = env::current_dir().context("retrieving current directory")?; let mut children = Vec::new(); let root; loop { // canonicalize handles './', and '/'. if let Ok(canonicalized) = fs::canonicalize(&path) { root = canonicalized; break; } // The comparison to `curdir` is just a shortcut // since we know it is canonical. The other one // is if `argument_str` is a string that starts // with a name (e.g. "foo/bar"). if path == curdir || path == Path::new("") { root = curdir; break; } children.push( path.file_name() .with_context(|| format!("parsing as path with position {argument_str}"))? .to_owned(), ); if !path.pop() { unreachable!("parsing as path with position {argument_str}"); } } Ok(children.iter().rev().fold(root, |mut path, child| { path.push(child); path })) }), } .map(|path_with_pos| path_with_pos.to_string(|path| path.to_string_lossy().into_owned()))} #[cfg(test)]mod tests { use super::*; use serde_json::json; use util::path; use util::paths::SanitizedPath; use util::test::TempTree; macro_rules! assert_path_eq { ($left:expr, $right:expr) => { assert_eq!( SanitizedPath::new(Path::new(&$left)), SanitizedPath::new(Path::new(&$right)) ) }; } fn cwd() -> PathBuf { env::current_dir().unwrap() } static CWD_LOCK: Mutex<()> = Mutex::new(()); fn with_cwd<T>(path: &Path, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> { let _lock = CWD_LOCK.lock(); let old_cwd = cwd(); env::set_current_dir(path)?; let result = f(); env::set_current_dir(old_cwd)?; result } #[test] fn test_parse_non_existing_path() { // Absolute path let result = parse_path_with_position(path!("/non/existing/path.txt")).unwrap(); assert_path_eq!(result, path!("/non/existing/path.txt")); // Absolute path in cwd let path = cwd().join(path!("non/existing/path.txt")); let expected = path.to_string_lossy().to_string(); let result = parse_path_with_position(&expected).unwrap(); assert_path_eq!(result, expected); // Relative path let result = parse_path_with_position(path!("non/existing/path.txt")).unwrap(); assert_path_eq!(result, expected) } #[test] fn test_parse_existing_path() { let temp_tree = TempTree::new(json!({ "file.txt": "", })); let file_path = temp_tree.path().join("file.txt"); let expected = file_path.to_string_lossy().to_string(); // Absolute path let result = parse_path_with_position(file_path.to_str().unwrap()).unwrap(); assert_path_eq!(result, expected); // Relative path let result = with_cwd(temp_tree.path(), || parse_path_with_position("file.txt")).unwrap(); assert_path_eq!(result, expected); } // NOTE: // While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus // we assume that they are not supported out of the box. #[cfg(not(windows))] #[test] fn test_parse_symlink_file() { let temp_tree = TempTree::new(json!({ "target.txt": "", })); let target_path = temp_tree.path().join("target.txt"); let symlink_path = temp_tree.path().join("symlink.txt"); std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap(); // Absolute path let result = parse_path_with_position(symlink_path.to_str().unwrap()).unwrap(); assert_eq!(result, target_path.to_string_lossy()); // Relative path let result = with_cwd(temp_tree.path(), || parse_path_with_position("symlink.txt")).unwrap(); assert_eq!(result, target_path.to_string_lossy()); } #[cfg(not(windows))] #[test] fn test_parse_symlink_dir() { let temp_tree = TempTree::new(json!({ "some": { "dir": { // symlink target "ec": { "tory": { "file.txt": "", }}}}})); let target_file_path = temp_tree.path().join("some/dir/ec/tory/file.txt"); let expected = target_file_path.to_string_lossy(); let dir_path = temp_tree.path().join("some/dir"); let symlink_path = temp_tree.path().join("symlink"); std::os::unix::fs::symlink(&dir_path, &symlink_path).unwrap(); // Absolute path let result = parse_path_with_position(symlink_path.join("ec/tory/file.txt").to_str().unwrap()) .unwrap(); assert_eq!(result, expected); // Relative path let result = with_cwd(temp_tree.path(), || { parse_path_with_position("symlink/ec/tory/file.txt") }) .unwrap(); assert_eq!(result, expected); }} fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> { let mut source = PathWithPosition::parse_str(source); let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { if user.is_empty() { anyhow::bail!("user is empty in wsl argument"); } (Some(user), distro) } else { (None, wsl) }; let mut args = vec!["--distribution", distro_name]; if let Some(user) = user { args.push("--user"); args.push(user); } let command = [ OsStr::new("realpath"), OsStr::new("-s"), source.path.as_ref(), ]; let output = util::command::new_std_command("wsl.exe") .args(&args) .arg("--exec") .args(&command) .output()?; let result = if output.status.success() { String::from_utf8_lossy(&output.stdout).to_string() } else { let fallback = util::command::new_std_command("wsl.exe") .args(&args) .arg("--") .args(&command) .output()?; String::from_utf8_lossy(&fallback.stdout).to_string() }; source.path = Path::new(result.trim()).to_owned(); Ok(source.to_string(|path| path.to_string_lossy().into_owned()))} fn main() -> Result<()> { #[cfg(unix)] util::prevent_root_execution(); // Exit flatpak sandbox if needed #[cfg(target_os = "linux")] { flatpak::try_restart_to_host(); flatpak::ld_extra_libs(); } // Intercept version designators #[cfg(target_os = "macos")] if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) { // When the first argument is a name of a release channel, we're going to spawn off the CLI of that version, with trailing args passed along. use std::str::FromStr as _; if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) { return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect()); } } let args = Args::parse(); // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass if let Some(socket) = &args.askpass { askpass::main(socket); return Ok(()); } // 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(target_os = "linux")] let args = flatpak::set_bin_if_no_escape(args); let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?; if args.version { println!("{}", app.zed_version_string()); return Ok(()); } if args.system_specs { let path = app.path(); let msg = [ "The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.", "To retrieve the system specs on the command line, run the following command:", &format!("{} --system-specs", path.display()), ]; anyhow::bail!(msg.join("\n")); } #[cfg(all( any(target_os = "linux", target_os = "macos"), not(feature = "no-bundled-uninstall") ))] if args.uninstall { static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh"); let tmp_dir = tempfile::tempdir()?; let script_path = tmp_dir.path().join("uninstall.sh"); fs::write(&script_path, UNINSTALL_SCRIPT)?; use std::os::unix::fs::PermissionsExt as _; fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?; let status = std::process::Command::new("sh") .arg(&script_path) .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME) .status() .context("Failed to execute uninstall script")?; std::process::exit(status.code().unwrap_or(1)); } let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?; let url = format!("zed-cli://{server_name}"); let open_new_workspace = if args.new { Some(true) } else if args.add { Some(false) } else { None }; let env = { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { use collections::HashMap; // On Linux, the desktop entry uses `cli` to spawn `zed`. // We need to handle env vars correctly since std::env::vars() may not contain // project-specific vars (e.g. those set by direnv). // By setting env to None here, the LSP will use worktree env vars instead, // which is what we want. if !std::io::stdout().is_terminal() { None } else { Some(std::env::vars().collect::<HashMap<_, _>>()) } } #[cfg(target_os = "windows")] { // On Windows, by default, a child process inherits a copy of the environment block of the parent process. // So we don't need to pass env vars explicitly. None } #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))] { use collections::HashMap; Some(std::env::vars().collect::<HashMap<_, _>>()) } }; let exit_status = Arc::new(Mutex::new(None)); let mut paths = vec![]; let mut urls = vec![]; let mut diff_paths = vec![]; let mut stdin_tmp_file: Option<fs::File> = None; let mut anonymous_fd_tmp_files = vec![]; for path in args.diff.chunks(2) { diff_paths.push([ parse_path_with_position(&path[0])?, parse_path_with_position(&path[1])?, ]); } #[cfg(target_os = "windows")] let wsl = args.wsl.as_ref(); #[cfg(not(target_os = "windows"))] let wsl = None; for path in args.paths_with_position.iter() { if URL_PREFIX.iter().any(|&prefix| path.starts_with(prefix)) { urls.push(path.to_string()); } else if path == "-" && args.paths_with_position.len() == 1 { let file = NamedTempFile::new()?; paths.push(file.path().to_string_lossy().into_owned()); let (file, _) = file.keep()?; stdin_tmp_file = Some(file); } else if let Some(file) = anonymous_fd(path) { let tmp_file = NamedTempFile::new()?;Lines 1–500 of 1252. Load more
#![allow( clippy::disallowed_methods, reason = "We are not in an async environment, so std::process::Command is fine" )] #![cfg_attr( any(target_os = "linux", target_os = "freebsd", target_os = "windows"), allow(dead_code) )] use anyhow::{Context as _, Result}; use clap::Parser; use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer}; use parking_lot::Mutex; use std::{ env, ffi::OsStr, fs, io, path::{Path, PathBuf}, process::ExitStatus, sync::Arc, thread::{self, JoinHandle}, }; use tempfile::NamedTempFile; use util::paths::PathWithPosition; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use std::io::IsTerminal; const URL_PREFIX: [&'static str; 5] = ["zed://", "http://", "https://", "file://", "ssh://"]; struct Detect; trait InstalledApp { fn zed_version_string(&self) -> String; fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()>; fn run_foreground( &self, ipc_url: String, user_data_dir: Option<&str>, ) -> io::Result<ExitStatus>; fn path(&self) -> PathBuf; } #[derive(Parser, Debug)] #[command( name = "zed", disable_version_flag = true, before_help = "The Zed CLI binary. This CLI is a separate binary that invokes Zed. Examples: `zed` Simply opens Zed `zed --foreground` Runs in foreground (shows all logs) `zed path-to-your-project` Open your project in Zed `zed -n path-to-file ` Open file/folder in a new window", after_help = "To read from stdin, append '-', e.g. 'ps axf | zed -'" )] struct Args { /// Wait for all of the given paths to be opened/closed before exiting. /// /// When opening a directory, waits until the created window is closed. #[arg(short, long)] wait: bool, /// Add files to the currently open workspace #[arg(short, long, overrides_with_all = ["new", "reuse"])] add: bool, /// Create a new workspace #[arg(short, long, overrides_with_all = ["add", "reuse"])] new: bool, /// Reuse an existing window, replacing its workspace #[arg(short, long, overrides_with_all = ["add", "new"])] reuse: bool, /// Sets a custom directory for all user data (e.g., database, extensions, logs). /// This overrides the default platform-specific data directory location: #[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")] #[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\Zed`.")] #[cfg_attr( not(any(target_os = "windows", target_os = "macos")), doc = "`$XDG_DATA_HOME/zed`." )] #[arg(long, value_name = "DIR")] user_data_dir: Option<String>, /// The paths to open in Zed (space-separated). /// /// Use `path:line:column` syntax to open a file at the given line and column. paths_with_position: Vec<String>, /// Print Zed's version and the app path. #[arg(short, long)] version: bool, /// Run zed in the foreground (useful for debugging) #[arg(long)] foreground: bool, /// Custom path to Zed.app or the zed binary #[arg(long)] zed: Option<PathBuf>, /// Run zed in dev-server mode #[arg(long)] dev_server_token: Option<String>, /// The username and WSL distribution to use when opening paths. If not specified, /// Zed will attempt to open the paths directly. /// /// The username is optional, and if not specified, the default user for the distribution /// will be used. /// /// Example: `me@Ubuntu` or `Ubuntu`. /// /// WARN: You should not fill in this field by hand. #[cfg(target_os = "windows")] #[arg(long, value_name = "USER@DISTRO")] wsl: Option<String>, /// Not supported in Zed CLI, only supported on Zed binary /// Will attempt to give the correct command to run #[arg(long)] system_specs: bool, /// Pairs of file paths to diff. Can be specified multiple times. #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] diff: Vec<String>, /// Uninstall Zed from user system #[cfg(all( any(target_os = "linux", target_os = "macos"), not(feature = "no-bundled-uninstall") ))] #[arg(long)] uninstall: bool, /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency, /// by having Zed act like netcat communicating over a Unix socket. #[arg(long, hide = true)] askpass: Option<String>, } /// Parses a path containing a position (e.g. `path:line:column`) /// and returns its canonicalized string representation. /// /// If a part of path doesn't exist, it will canonicalize the /// existing part and append the non-existing part. /// /// This method must return an absolute path, as many zed /// crates assume absolute paths. fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> { match Path::new(argument_str).canonicalize() { Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)), Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| { let curdir = env::current_dir().context("retrieving current directory")?; let mut children = Vec::new(); let root; loop { // canonicalize handles './', and '/'. if let Ok(canonicalized) = fs::canonicalize(&path) { root = canonicalized; break; } // The comparison to `curdir` is just a shortcut // since we know it is canonical. The other one // is if `argument_str` is a string that starts // with a name (e.g. "foo/bar"). if path == curdir || path == Path::new("") { root = curdir; break; } children.push( path.file_name() .with_context(|| format!("parsing as path with position {argument_str}"))? .to_owned(), ); if !path.pop() { unreachable!("parsing as path with position {argument_str}"); } } Ok(children.iter().rev().fold(root, |mut path, child| { path.push(child); path })) }), } .map(|path_with_pos| path_with_pos.to_string(|path| path.to_string_lossy().into_owned())) } #[cfg(test)] mod tests { use super::*; use serde_json::json; use util::path; use util::paths::SanitizedPath; use util::test::TempTree; macro_rules! assert_path_eq { ($left:expr, $right:expr) => { assert_eq!( SanitizedPath::new(Path::new(&$left)), SanitizedPath::new(Path::new(&$right)) ) }; } fn cwd() -> PathBuf { env::current_dir().unwrap() } static CWD_LOCK: Mutex<()> = Mutex::new(()); fn with_cwd<T>(path: &Path, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> { let _lock = CWD_LOCK.lock(); let old_cwd = cwd(); env::set_current_dir(path)?; let result = f(); env::set_current_dir(old_cwd)?; result } #[test] fn test_parse_non_existing_path() { // Absolute path let result = parse_path_with_position(path!("/non/existing/path.txt")).unwrap(); assert_path_eq!(result, path!("/non/existing/path.txt")); // Absolute path in cwd let path = cwd().join(path!("non/existing/path.txt")); let expected = path.to_string_lossy().to_string(); let result = parse_path_with_position(&expected).unwrap(); assert_path_eq!(result, expected); // Relative path let result = parse_path_with_position(path!("non/existing/path.txt")).unwrap(); assert_path_eq!(result, expected) } #[test] fn test_parse_existing_path() { let temp_tree = TempTree::new(json!({ "file.txt": "", })); let file_path = temp_tree.path().join("file.txt"); let expected = file_path.to_string_lossy().to_string(); // Absolute path let result = parse_path_with_position(file_path.to_str().unwrap()).unwrap(); assert_path_eq!(result, expected); // Relative path let result = with_cwd(temp_tree.path(), || parse_path_with_position("file.txt")).unwrap(); assert_path_eq!(result, expected); } // NOTE: // While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus // we assume that they are not supported out of the box. #[cfg(not(windows))] #[test] fn test_parse_symlink_file() { let temp_tree = TempTree::new(json!({ "target.txt": "", })); let target_path = temp_tree.path().join("target.txt"); let symlink_path = temp_tree.path().join("symlink.txt"); std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap(); // Absolute path let result = parse_path_with_position(symlink_path.to_str().unwrap()).unwrap(); assert_eq!(result, target_path.to_string_lossy()); // Relative path let result = with_cwd(temp_tree.path(), || parse_path_with_position("symlink.txt")).unwrap(); assert_eq!(result, target_path.to_string_lossy()); } #[cfg(not(windows))] #[test] fn test_parse_symlink_dir() { let temp_tree = TempTree::new(json!({ "some": { "dir": { // symlink target "ec": { "tory": { "file.txt": "", }}}}})); let target_file_path = temp_tree.path().join("some/dir/ec/tory/file.txt"); let expected = target_file_path.to_string_lossy(); let dir_path = temp_tree.path().join("some/dir"); let symlink_path = temp_tree.path().join("symlink"); std::os::unix::fs::symlink(&dir_path, &symlink_path).unwrap(); // Absolute path let result = parse_path_with_position(symlink_path.join("ec/tory/file.txt").to_str().unwrap()) .unwrap(); assert_eq!(result, expected); // Relative path let result = with_cwd(temp_tree.path(), || { parse_path_with_position("symlink/ec/tory/file.txt") }) .unwrap(); assert_eq!(result, expected); } } fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> { let mut source = PathWithPosition::parse_str(source); let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { if user.is_empty() { anyhow::bail!("user is empty in wsl argument"); } (Some(user), distro) } else { (None, wsl) }; let mut args = vec!["--distribution", distro_name]; if let Some(user) = user { args.push("--user"); args.push(user); } let command = [ OsStr::new("realpath"), OsStr::new("-s"), source.path.as_ref(), ]; let output = util::command::new_std_command("wsl.exe") .args(&args) .arg("--exec") .args(&command) .output()?; let result = if output.status.success() { String::from_utf8_lossy(&output.stdout).to_string() } else { let fallback = util::command::new_std_command("wsl.exe") .args(&args) .arg("--") .args(&command) .output()?; String::from_utf8_lossy(&fallback.stdout).to_string() }; source.path = Path::new(result.trim()).to_owned(); Ok(source.to_string(|path| path.to_string_lossy().into_owned())) } fn main() -> Result<()> { #[cfg(unix)] util::prevent_root_execution(); // Exit flatpak sandbox if needed #[cfg(target_os = "linux")] { flatpak::try_restart_to_host(); flatpak::ld_extra_libs(); } // Intercept version designators #[cfg(target_os = "macos")] if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) { // When the first argument is a name of a release channel, we're going to spawn off the CLI of that version, with trailing args passed along. use std::str::FromStr as _; if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) { return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect()); } } let args = Args::parse(); // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass if let Some(socket) = &args.askpass { askpass::main(socket); return Ok(()); } // 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(target_os = "linux")] let args = flatpak::set_bin_if_no_escape(args); let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?; if args.version { println!("{}", app.zed_version_string()); return Ok(()); } if args.system_specs { let path = app.path(); let msg = [ "The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.", "To retrieve the system specs on the command line, run the following command:", &format!("{} --system-specs", path.display()), ]; anyhow::bail!(msg.join("\n")); } #[cfg(all( any(target_os = "linux", target_os = "macos"), not(feature = "no-bundled-uninstall") ))] if args.uninstall { static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh"); let tmp_dir = tempfile::tempdir()?; let script_path = tmp_dir.path().join("uninstall.sh"); fs::write(&script_path, UNINSTALL_SCRIPT)?; use std::os::unix::fs::PermissionsExt as _; fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?; let status = std::process::Command::new("sh") .arg(&script_path) .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME) .status() .context("Failed to execute uninstall script")?; std::process::exit(status.code().unwrap_or(1)); } let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?; let url = format!("zed-cli://{server_name}"); let open_new_workspace = if args.new { Some(true) } else if args.add { Some(false) } else { None }; let env = { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { use collections::HashMap; // On Linux, the desktop entry uses `cli` to spawn `zed`. // We need to handle env vars correctly since std::env::vars() may not contain // project-specific vars (e.g. those set by direnv). // By setting env to None here, the LSP will use worktree env vars instead, // which is what we want. if !std::io::stdout().is_terminal() { None } else { Some(std::env::vars().collect::<HashMap<_, _>>()) } } #[cfg(target_os = "windows")] { // On Windows, by default, a child process inherits a copy of the environment block of the parent process. // So we don't need to pass env vars explicitly. None } #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))] { use collections::HashMap; Some(std::env::vars().collect::<HashMap<_, _>>()) } }; let exit_status = Arc::new(Mutex::new(None)); let mut paths = vec![]; let mut urls = vec![]; let mut diff_paths = vec![]; let mut stdin_tmp_file: Option<fs::File> = None; let mut anonymous_fd_tmp_files = vec![]; for path in args.diff.chunks(2) { diff_paths.push([ parse_path_with_position(&path[0])?, parse_path_with_position(&path[1])?, ]); } #[cfg(target_os = "windows")] let wsl = args.wsl.as_ref(); #[cfg(not(target_os = "windows"))] let wsl = None; for path in args.paths_with_position.iter() { if URL_PREFIX.iter().any(|&prefix| path.starts_with(prefix)) { urls.push(path.to_string()); } else if path == "-" && args.paths_with_position.len() == 1 { let file = NamedTempFile::new()?; paths.push(file.path().to_string_lossy().into_owned()); let (file, _) = file.keep()?; stdin_tmp_file = Some(file); } else if let Some(file) = anonymous_fd(path) { let tmp_file = NamedTempFile::new()?;