#![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::{ collections::{BTreeMap, BTreeSet}, env, ffi::OsStr, fs, io, path::{Path, PathBuf}, process::ExitStatus, sync::Arc, thread::{self, JoinHandle},};use tempfile::{NamedTempFile, TempDir};use util::paths::PathWithPosition;use walkdir::WalkDir; #[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. /// When directories are provided, recurses into them and shows all changed files in a single multi-diff view. #[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()))} fn expand_directory_diff_pairs( diff_pairs: Vec<[String; 2]>,) -> anyhow::Result<(Vec<[String; 2]>, Vec<TempDir>)> { let mut expanded = Vec::new(); let mut temp_dirs = Vec::new(); for pair in diff_pairs { let left = PathBuf::from(&pair[0]); let right = PathBuf::from(&pair[1]); if left.is_dir() && right.is_dir() { let (mut pairs, temp_dir) = expand_directory_pair(&left, &right)?; expanded.append(&mut pairs); if let Some(temp_dir) = temp_dir { temp_dirs.push(temp_dir); } } else { expanded.push(pair); } } Ok((expanded, temp_dirs))} fn expand_directory_pair( left: &Path, right: &Path,) -> anyhow::Result<(Vec<[String; 2]>, Option<TempDir>)> { let left_files = collect_files(left)?; let right_files = collect_files(right)?; let mut rel_paths = BTreeSet::new(); rel_paths.extend(left_files.keys().cloned()); rel_paths.extend(right_files.keys().cloned()); let mut temp_dir = TempDir::new()?; let mut temp_dir_used = false; let mut pairs = Vec::new(); for rel in rel_paths { match (left_files.get(&rel), right_files.get(&rel)) { (Some(left_path), Some(right_path)) => { pairs.push([ left_path.to_string_lossy().into_owned(), right_path.to_string_lossy().into_owned(), ]); } (Some(left_path), None) => { let stub = create_empty_stub(&mut temp_dir, &rel)?; temp_dir_used = true; pairs.push([ left_path.to_string_lossy().into_owned(), stub.to_string_lossy().into_owned(), ]); } (None, Some(right_path)) => { let stub = create_empty_stub(&mut temp_dir, &rel)?; temp_dir_used = true; pairs.push([ stub.to_string_lossy().into_owned(), right_path.to_string_lossy().into_owned(), ]); } (None, None) => {} } } let temp_dir = if temp_dir_used { Some(temp_dir) } else { None }; Ok((pairs, temp_dir))} fn collect_files(root: &Path) -> anyhow::Result<BTreeMap<PathBuf, PathBuf>> { let mut files = BTreeMap::new(); for entry in WalkDir::new(root) { let entry = entry?; if entry.file_type().is_file() { let rel = entry .path() .strip_prefix(root) .context("stripping directory prefix")? .to_path_buf(); files.insert(rel, entry.into_path()); } } Ok(files)} fn create_empty_stub(temp_dir: &mut TempDir, rel: &Path) -> anyhow::Result<PathBuf> { let stub_path = temp_dir.path().join(rel); if let Some(parent) = stub_path.parent() { fs::create_dir_all(parent)?; } fs::File::create(&stub_path)?; Ok(stub_path)} #[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:",Lines 1–500 of 1378. 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::{ collections::{BTreeMap, BTreeSet}, env, ffi::OsStr, fs, io, path::{Path, PathBuf}, process::ExitStatus, sync::Arc, thread::{self, JoinHandle}, }; use tempfile::{NamedTempFile, TempDir}; use util::paths::PathWithPosition; use walkdir::WalkDir; #[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. /// When directories are provided, recurses into them and shows all changed files in a single multi-diff view. #[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())) } fn expand_directory_diff_pairs( diff_pairs: Vec<[String; 2]>, ) -> anyhow::Result<(Vec<[String; 2]>, Vec<TempDir>)> { let mut expanded = Vec::new(); let mut temp_dirs = Vec::new(); for pair in diff_pairs { let left = PathBuf::from(&pair[0]); let right = PathBuf::from(&pair[1]); if left.is_dir() && right.is_dir() { let (mut pairs, temp_dir) = expand_directory_pair(&left, &right)?; expanded.append(&mut pairs); if let Some(temp_dir) = temp_dir { temp_dirs.push(temp_dir); } } else { expanded.push(pair); } } Ok((expanded, temp_dirs)) } fn expand_directory_pair( left: &Path, right: &Path, ) -> anyhow::Result<(Vec<[String; 2]>, Option<TempDir>)> { let left_files = collect_files(left)?; let right_files = collect_files(right)?; let mut rel_paths = BTreeSet::new(); rel_paths.extend(left_files.keys().cloned()); rel_paths.extend(right_files.keys().cloned()); let mut temp_dir = TempDir::new()?; let mut temp_dir_used = false; let mut pairs = Vec::new(); for rel in rel_paths { match (left_files.get(&rel), right_files.get(&rel)) { (Some(left_path), Some(right_path)) => { pairs.push([ left_path.to_string_lossy().into_owned(), right_path.to_string_lossy().into_owned(), ]); } (Some(left_path), None) => { let stub = create_empty_stub(&mut temp_dir, &rel)?; temp_dir_used = true; pairs.push([ left_path.to_string_lossy().into_owned(), stub.to_string_lossy().into_owned(), ]); } (None, Some(right_path)) => { let stub = create_empty_stub(&mut temp_dir, &rel)?; temp_dir_used = true; pairs.push([ stub.to_string_lossy().into_owned(), right_path.to_string_lossy().into_owned(), ]); } (None, None) => {} } } let temp_dir = if temp_dir_used { Some(temp_dir) } else { None }; Ok((pairs, temp_dir)) } fn collect_files(root: &Path) -> anyhow::Result<BTreeMap<PathBuf, PathBuf>> { let mut files = BTreeMap::new(); for entry in WalkDir::new(root) { let entry = entry?; if entry.file_type().is_file() { let rel = entry .path() .strip_prefix(root) .context("stripping directory prefix")? .to_path_buf(); files.insert(rel, entry.into_path()); } } Ok(files) } fn create_empty_stub(temp_dir: &mut TempDir, rel: &Path) -> anyhow::Result<PathBuf> { let stub_path = temp_dir.path().join(rel); if let Some(parent) = stub_path.parent() { fs::create_dir_all(parent)?; } fs::File::create(&stub_path)?; Ok(stub_path) } #[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:",