1378 lines · 46.5 KB · Rust
RawDownload
#![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.