1345 lines · 41.4 KB · Rust
RawDownload
pub mod arc_cow;pub mod archive;pub mod command;pub mod fs;pub mod markdown;pub mod paths;pub mod process;pub mod redact;pub mod rel_path;pub mod schemars;pub mod serde;pub mod shell;pub mod shell_builder;pub mod shell_env;pub mod size;#[cfg(any(test, feature = "test-support"))]pub mod test;pub mod time; use anyhow::{Context as _, Result};use futures::Future;use itertools::Either;use paths::PathExt;use regex::Regex;use std::path::PathBuf;use std::sync::{LazyLock, OnceLock};use std::{    borrow::Cow,    cmp::{self, Ordering},    env,    ops::{AddAssign, Range, RangeInclusive},    panic::Location,    pin::Pin,    task::{Context, Poll},    time::Instant,};use unicase::UniCase; pub use take_until::*;#[cfg(any(test, feature = "test-support"))]pub use util_macros::{line_endings, path, uri}; #[macro_export]macro_rules! debug_panic {    ( $($fmt_arg:tt)* ) => {        if cfg!(debug_assertions) {            panic!( $($fmt_arg)* );        } else {            let backtrace = std::backtrace::Backtrace::capture();            log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace);        }    };} #[inline]pub const fn is_utf8_char_boundary(u8: u8) -> bool {    // This is bit magic equivalent to: b < 128 || b >= 192    (u8 as i8) >= -0x40} pub fn truncate(s: &str, max_chars: usize) -> &str {    match s.char_indices().nth(max_chars) {        None => s,        Some((idx, _)) => &s[..idx],    }} /// Removes characters from the end of the string if its length is greater than `max_chars` and/// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars.pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {    debug_assert!(max_chars >= 5);     // If the string's byte length is <= max_chars, walking the string can be skipped since the    // number of chars is <= the number of bytes.    if s.len() <= max_chars {        return s.to_string();    }    let truncation_ix = s.char_indices().map(|(i, _)| i).nth(max_chars);    match truncation_ix {        Some(index) => s[..index].to_string() + "…",        _ => s.to_string(),    }} /// Removes characters from the front of the string if its length is greater than `max_chars` and/// prepends the string with "...". Returns string unchanged if its length is smaller than max_chars.pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String {    debug_assert!(max_chars >= 5);     // If the string's byte length is <= max_chars, walking the string can be skipped since the    // number of chars is <= the number of bytes.    if s.len() <= max_chars {        return s.to_string();    }    let suffix_char_length = max_chars.saturating_sub(1);    let truncation_ix = s        .char_indices()        .map(|(i, _)| i)        .nth_back(suffix_char_length);    match truncation_ix {        Some(index) if index > 0 => "…".to_string() + &s[index..],        _ => s.to_string(),    }} /// Takes only `max_lines` from the string and, if there were more than `max_lines-1`, appends a/// a newline and "..." to the string, so that `max_lines` are returned./// Returns string unchanged if its length is smaller than max_lines.pub fn truncate_lines_and_trailoff(s: &str, max_lines: usize) -> String {    let mut lines = s.lines().take(max_lines).collect::<Vec<_>>();    if lines.len() > max_lines - 1 {        lines.pop();        lines.join("\n") + "\n…"    } else {        lines.join("\n")    }} /// Truncates the string at a character boundary, such that the result is less than `max_bytes` in/// length.pub fn truncate_to_byte_limit(s: &str, max_bytes: usize) -> &str {    if s.len() < max_bytes {        return s;    }     for i in (0..max_bytes).rev() {        if s.is_char_boundary(i) {            return &s[..i];        }    }     ""} /// Takes a prefix of complete lines which fit within the byte limit. If the first line is longer/// than the limit, truncates at a character boundary.pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str {    if s.len() < max_bytes {        return s;    }     for i in (0..max_bytes).rev() {        if s.is_char_boundary(i) && s.as_bytes()[i] == b'\n' {            // Since the i-th character is \n, valid to slice at i + 1.            return &s[..i + 1];        }    }     truncate_to_byte_limit(s, max_bytes)} #[test]fn test_truncate_lines_to_byte_limit() {    let text = "Line 1\nLine 2\nLine 3\nLine 4";     // Limit that includes all lines    assert_eq!(truncate_lines_to_byte_limit(text, 100), text);     // Exactly the first line    assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n");     // Limit between lines    assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n");    assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n");     // Limit before first newline    assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line ");     // Test with non-ASCII characters    let text_utf8 = "Line 1\nLíne 2\nLine 3";    assert_eq!(        truncate_lines_to_byte_limit(text_utf8, 15),        "Line 1\nLíne 2\n"    );} pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {    let prev = *value;    *value += T::from(1);    prev} /// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and/// enforcing a maximum length. This also de-duplicates items. Sort the items according to the given callback. Before calling this,/// both `vec` and `new_items` should already be sorted according to the `cmp` comparator.pub fn extend_sorted<T, I, F>(vec: &mut Vec<T>, new_items: I, limit: usize, mut cmp: F)where    I: IntoIterator<Item = T>,    F: FnMut(&T, &T) -> Ordering,{    let mut start_index = 0;    for new_item in new_items {        if let Err(i) = vec[start_index..].binary_search_by(|m| cmp(m, &new_item)) {            let index = start_index + i;            if vec.len() < limit {                vec.insert(index, new_item);            } else if index < vec.len() {                vec.pop();                vec.insert(index, new_item);            }            start_index = index;        }    }} pub fn truncate_to_bottom_n_sorted_by<T, F>(items: &mut Vec<T>, limit: usize, compare: &F)where    F: Fn(&T, &T) -> Ordering,{    if limit == 0 {        items.truncate(0);    }    if items.len() <= limit {        items.sort_by(compare);        return;    }    // When limit is near to items.len() it may be more efficient to sort the whole list and    // truncate, rather than always doing selection first as is done below. It's hard to analyze    // where the threshold for this should be since the quickselect style algorithm used by    // `select_nth_unstable_by` makes the prefix partially sorted, and so its work is not wasted -    // the expected number of comparisons needed by `sort_by` is less than it is for some arbitrary    // unsorted input.    items.select_nth_unstable_by(limit, compare);    items.truncate(limit);    items.sort_by(compare);} /// Prevents execution of the application with root privileges on Unix systems.////// This function checks if the current process is running with root privileges/// and terminates the program with an error message unless explicitly allowed via the/// `ZED_ALLOW_ROOT` environment variable.#[cfg(unix)]pub fn prevent_root_execution() {    let is_root = nix::unistd::geteuid().is_root();    let allow_root = std::env::var("ZED_ALLOW_ROOT").is_ok_and(|val| val == "true");     if is_root && !allow_root {        eprintln!(            "\Error: Running Zed as root or via sudo is unsupported.       Doing so (even once) may subtly break things for all subsequent non-root usage of Zed.       It is untested and not recommended, don't complain when things break.       If you wish to proceed anyways, set `ZED_ALLOW_ROOT=true` in your environment."        );        std::process::exit(1);    }} #[cfg(unix)]fn load_shell_from_passwd() -> Result<()> {    let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } {        n if n < 0 => 1024,        n => n as usize,    };    let mut buffer = Vec::with_capacity(buflen);     let mut pwd: std::mem::MaybeUninit<libc::passwd> = std::mem::MaybeUninit::uninit();    let mut result: *mut libc::passwd = std::ptr::null_mut();     let uid = unsafe { libc::getuid() };    let status = unsafe {        libc::getpwuid_r(            uid,            pwd.as_mut_ptr(),            buffer.as_mut_ptr() as *mut libc::c_char,            buflen,            &mut result,        )    };    anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid);     // SAFETY: If `getpwuid_r` doesn't error, we have the entry here.    let entry = unsafe { pwd.assume_init() };     anyhow::ensure!(        status == 0,        "call to getpwuid_r failed. uid: {}, status: {}",        uid,        status    );    anyhow::ensure!(        entry.pw_uid == uid,        "passwd entry has different uid ({}) than getuid ({}) returned",        entry.pw_uid,        uid,    );     let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() };    let should_set_shell = env::var("SHELL").map_or(true, |shell_env| {        shell_env != shell && !std::path::Path::new(&shell_env).exists()    });     if should_set_shell {        log::info!(            "updating SHELL environment variable to value from passwd entry: {:?}",            shell,        );        unsafe { env::set_var("SHELL", shell) };    }     Ok(())} /// Returns a shell escaped path for the current zed executablepub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result<String> {    let mut zed_path =        std::env::current_exe().context("Failed to determine current zed executable path.")?;    if cfg!(target_os = "linux")        && !zed_path.is_file()        && let Some(truncated) = zed_path            .clone()            .file_name()            .and_then(|s| s.to_str())            .and_then(|n| n.strip_suffix(" (deleted)"))    {        // Might have been deleted during update; let's use the new binary if there is one.        zed_path.set_file_name(truncated);    }     zed_path        .try_shell_safe(shell_kind)        .context("Failed to shell-escape Zed executable path.")} /// Returns a path for the zed cli executable, this function/// should be called from the zed executable, not zed-cli.pub fn get_zed_cli_path() -> Result<PathBuf> {    let zed_path =        std::env::current_exe().context("Failed to determine current zed executable path.")?;    let parent = zed_path        .parent()        .context("Failed to determine parent directory of zed executable path.")?;     let possible_locations: &[&str] = if cfg!(target_os = "macos") {        // On macOS, the zed executable and zed-cli are inside the app bundle,        // so here ./cli is for both installed and development builds.        &["./cli"]    } else if cfg!(target_os = "windows") {        // bin/zed.exe is for installed builds, ./cli.exe is for development builds.        &["bin/zed.exe", "./cli.exe"]    } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") {        // bin is the standard, ./cli is for the target directory in development builds.        &["../bin/zed", "./cli"]    } else {        anyhow::bail!("unsupported platform for determining zed-cli path");    };     possible_locations        .iter()        .find_map(|p| {            parent                .join(p)                .canonicalize()                .ok()                .filter(|p| p != &zed_path)        })        .with_context(|| {            format!(                "could not find zed-cli from any of: {}",                possible_locations.join(", ")            )        })} #[cfg(unix)]pub async fn load_login_shell_environment() -> Result<()> {    load_shell_from_passwd().log_err();     // If possible, we want to `cd` in the user's `$HOME` to trigger programs    // such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook    // into shell's `cd` command (and hooks) to manipulate env.    // We do this so that we get the env a user would have when spawning a shell    // in home directory.    for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir())        .await        .with_context(|| format!("capturing environment with {:?}", get_system_shell()))?    {        // Skip SHLVL to prevent it from polluting Zed's process environment.        // The login shell used for env capture increments SHLVL, and if we propagate it,        // terminals spawned by Zed will inherit it and increment again, causing SHLVL        // to start at 2 instead of 1 (and increase by 2 on each reload).        if name == "SHLVL" {            continue;        }        unsafe { env::set_var(&name, &value) };    }     log::info!(        "set environment variables from shell:{}, path:{}",        std::env::var("SHELL").unwrap_or_default(),        std::env::var("PATH").unwrap_or_default(),    );     Ok(())} /// Configures the process to start a new session, to prevent interactive shells from taking control/// of the terminal.////// For more details: <https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell>pub fn set_pre_exec_to_start_new_session(    command: &mut std::process::Command,) -> &mut std::process::Command {    // safety: code in pre_exec should be signal safe.    // https://man7.org/linux/man-pages/man7/signal-safety.7.html    #[cfg(not(target_os = "windows"))]    unsafe {        use std::os::unix::process::CommandExt;        command.pre_exec(|| {            libc::setsid();            #[cfg(target_os = "macos")]            crate::command::reset_exception_ports();            Ok(())        });    };    command} pub fn merge_json_lenient_value_into(    source: serde_json_lenient::Value,    target: &mut serde_json_lenient::Value,) {    match (source, target) {        (serde_json_lenient::Value::Object(source), serde_json_lenient::Value::Object(target)) => {            for (key, value) in source {                if let Some(target) = target.get_mut(&key) {                    merge_json_lenient_value_into(value, target);                } else {                    target.insert(key, value);                }            }        }         (serde_json_lenient::Value::Array(source), serde_json_lenient::Value::Array(target)) => {            for value in source {                target.push(value);            }        }         (source, target) => *target = source,    }} pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {    use serde_json::Value;     match (source, target) {        (Value::Object(source), Value::Object(target)) => {            for (key, value) in source {                if let Some(target) = target.get_mut(&key) {                    merge_json_value_into(value, target);                } else {                    target.insert(key, value);                }            }        }         (Value::Array(source), Value::Array(target)) => {            for value in source {                target.push(value);            }        }         (source, target) => *target = source,    }} pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {    use serde_json::Value;    if let Value::Object(source_object) = source {        let target_object = if let Value::Object(target) = target {            target        } else {            *target = Value::Object(Default::default());            target.as_object_mut().unwrap()        };        for (key, value) in source_object {            if let Some(target) = target_object.get_mut(&key) {                merge_non_null_json_value_into(value, target);            } else if !value.is_null() {                target_object.insert(key, value);            }        }    } else if !source.is_null() {        *target = source    }} pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {    static ZED_MEASUREMENTS: OnceLock<bool> = OnceLock::new();    let zed_measurements = ZED_MEASUREMENTS.get_or_init(|| {        env::var("ZED_MEASUREMENTS")            .map(|measurements| measurements == "1" || measurements == "true")            .unwrap_or(false)    });     if *zed_measurements {        let start = Instant::now();        let result = f();

Lines 1–500 of 1345.