1367 lines · 41.8 KB · Rust
RawDownload
pub mod arc_cow;pub mod archive;pub mod command;pub mod fs;pub mod markdown;pub mod paths;pub mod redact;pub mod schemars;pub mod serde;pub mod shell_env;pub mod size;#[cfg(any(test, feature = "test-support"))]pub mod test;pub mod time; use anyhow::Result;use futures::Future;use itertools::Either;use regex::Regex;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);        }    };} 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) {            if 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,        )    };    let entry = unsafe { pwd.assume_init() };     anyhow::ensure!(        status == 0,        "call to getpwuid_r failed. uid: {}, status: {}",        uid,        status    );    anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid);    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() };    if env::var("SHELL").map_or(true, |shell_env| shell_env != shell) {        log::info!(            "updating SHELL environment variable to value from passwd entry: {:?}",            shell,        );        unsafe { env::set_var("SHELL", shell) };    }     Ok(())} #[cfg(unix)]/// Returns a shell escaped path for the current zed executablepub fn get_shell_safe_zed_path() -> anyhow::Result<String> {    use anyhow::Context;     let zed_path = std::env::current_exe()        .context("Failed to determine current zed executable path.")?        .to_string_lossy()        .trim_end_matches(" (deleted)") // see https://github.com/rust-lang/rust/issues/69343        .to_string();     // As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible    // but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other    // errors are introduced in the future :(    let zed_path_escaped =        shlex::try_quote(&zed_path).context("Failed to shell-escape Zed executable path.")?;     return Ok(zed_path_escaped.to_string());} #[cfg(unix)]pub 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(paths::home_dir())? {        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-shellpub 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();            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();        let elapsed = start.elapsed();        eprintln!("{}: {:?}", label, elapsed);        result    } else {        f()    }} pub fn expanded_and_wrapped_usize_range(    range: Range<usize>,    additional_before: usize,    additional_after: usize,    wrap_length: usize,) -> impl Iterator<Item = usize> {    let start_wraps = range.start < additional_before;    let end_wraps = wrap_length < range.end + additional_after;    if start_wraps && end_wraps {        Either::Left(0..wrap_length)    } else if start_wraps {        let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before);        if wrapped_start <= range.end {            Either::Left(0..wrap_length)        } else {            Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length))        }    } else if end_wraps {        let wrapped_end = range.end + additional_after - wrap_length;        if range.start <= wrapped_end {            Either::Left(0..wrap_length)        } else {            Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length))        }    } else {        Either::Left((range.start - additional_before)..(range.end + additional_after))    }} /// Yields `[i, i + 1, i - 1, i + 2, ..]`, each modulo `wrap_length` and bounded by/// `additional_before` and `additional_after`. If the wrapping causes overlap, duplicates are not/// emitted. If wrap_length is 0, nothing is yielded.pub fn wrapped_usize_outward_from(    start: usize,    additional_before: usize,    additional_after: usize,    wrap_length: usize,) -> impl Iterator<Item = usize> {    let mut count = 0;    let mut after_offset = 1;    let mut before_offset = 1;     std::iter::from_fn(move || {        count += 1;        if count > wrap_length {            None        } else if count == 1 {            Some(start % wrap_length)        } else if after_offset <= additional_after && after_offset <= before_offset {            let value = (start + after_offset) % wrap_length;            after_offset += 1;            Some(value)        } else if before_offset <= additional_before {            let value = (start + wrap_length - before_offset) % wrap_length;            before_offset += 1;            Some(value)        } else if after_offset <= additional_after {            let value = (start + after_offset) % wrap_length;            after_offset += 1;            Some(value)        } else {

Lines 1–500 of 1367.