1239 lines · 38.0 KB · Rust
RawDownload
pub mod arc_cow;pub mod command;pub mod fs;pub mod markdown;pub mod paths;pub mod serde;#[cfg(any(test, feature = "test-support"))]pub mod test; 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; #[cfg(unix)]use anyhow::{Context as _, anyhow}; pub use take_until::*;#[cfg(any(test, feature = "test-support"))]pub use util_macros::{line_endings, separator, 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);        }    };} /// A macro to add "C:" to the beginning of a path literal on Windows, and replace all/// the separator from `/` to `\`./// But on non-Windows platforms, it will return the path literal as is.////// # Examples/// ```rust/// use util::path;////// let path = path!("/Users/user/file.txt");/// #[cfg(target_os = "windows")]/// assert_eq!(path, "C:\\Users\\user\\file.txt");/// #[cfg(not(target_os = "windows"))]/// assert_eq!(path, "/Users/user/file.txt");/// ```#[cfg(all(any(test, feature = "test-support"), target_os = "windows"))]#[macro_export]macro_rules! path {    ($path:literal) => {        concat!("C:", util::separator!($path))    };} /// A macro to add "C:" to the beginning of a path literal on Windows, and replace all/// the separator from `/` to `\`./// But on non-Windows platforms, it will return the path literal as is.////// # Examples/// ```rust/// use util::path;////// let path = path!("/Users/user/file.txt");/// #[cfg(target_os = "windows")]/// assert_eq!(path, "C:\\Users\\user\\file.txt");/// #[cfg(not(target_os = "windows"))]/// assert_eq!(path, "/Users/user/file.txt");/// ```#[cfg(all(any(test, feature = "test-support"), not(target_os = "windows")))]#[macro_export]macro_rules! path {    ($path:literal) => {        $path    };} 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);} #[cfg(unix)]pub 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)]pub fn load_login_shell_environment() -> Result<()> {    let marker = "ZED_LOGIN_SHELL_START";    let shell = env::var("SHELL").context(        "SHELL environment variable is not assigned so we can't source login environment variables",    )?;     // 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.    let shell_cmd_prefix = std::env::var_os("HOME")        .and_then(|home| home.into_string().ok())        .map(|home| format!("cd '{home}';"));     // The `exit 0` is the result of hours of debugging, trying to find out    // why running this command here, without `exit 0`, would mess    // up signal process for our process so that `ctrl-c` doesn't work    // anymore.    // We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'`  would    // do that, but it does, and `exit 0` helps.    let shell_cmd = format!(        "{}printf '%s' {marker}; /usr/bin/env; exit 0;",        shell_cmd_prefix.as_deref().unwrap_or("")    );     let output = std::process::Command::new(&shell)        .args(["-l", "-i", "-c", &shell_cmd])        .output()        .context("failed to spawn login shell to source login environment variables")?;    if !output.status.success() {        Err(anyhow!("login shell exited with error"))?;    }     let stdout = String::from_utf8_lossy(&output.stdout);     if let Some(env_output_start) = stdout.find(marker) {        let env_output = &stdout[env_output_start + marker.len()..];         parse_env_output(env_output, |key, value| unsafe { env::set_var(key, value) });         log::info!(            "set environment variables from shell:{}, path:{}",            shell,            env::var("PATH").unwrap_or_default(),        );    }     Ok(())} /// Parse the result of calling `usr/bin/env` with no argumentspub fn parse_env_output(env: &str, mut f: impl FnMut(String, String)) {    let mut current_key: Option<String> = None;    let mut current_value: Option<String> = None;     for line in env.split_terminator('\n') {        if let Some(separator_index) = line.find('=') {            if !line[..separator_index].is_empty() {                if let Some((key, value)) = Option::zip(current_key.take(), current_value.take()) {                    f(key, value)                }                current_key = Some(line[..separator_index].to_string());                current_value = Some(line[separator_index + 1..].to_string());                continue;            };        }        if let Some(value) = current_value.as_mut() {            value.push('\n');            value.push_str(line);        }    }    if let Some((key, value)) = Option::zip(current_key.take(), current_value.take()) {        f(key, value)    }} 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 iterate_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))    }} #[cfg(target_os = "windows")]pub fn get_windows_system_shell() -> String {    use std::path::PathBuf;     fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {        #[cfg(target_pointer_width = "64")]        let env_var = if find_alternate {            "ProgramFiles(x86)"        } else {            "ProgramFiles"        };         #[cfg(target_pointer_width = "32")]        let env_var = if find_alternate {            "ProgramW6432"        } else {            "ProgramFiles"        };         let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell");        install_base_dir            .read_dir()

Lines 1–500 of 1239.