1756 lines · 54.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 serde;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::num::NonZeroU32;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 _; 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)} fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {    let tab_size = tab_size.get() as usize;    let mut width = offset;     for ch in text.chars() {        width += if ch == '\t' {            tab_size - (width % tab_size)        } else {            1        };    }     width - offset} /// Tokenizes a string into runs of text that should stick together, or that is whitespace.struct WordBreakingTokenizer<'a> {    input: &'a str,} impl<'a> WordBreakingTokenizer<'a> {    fn new(input: &'a str) -> Self {        Self { input }    }} fn is_char_ideographic(ch: char) -> bool {    use unicode_script::Script::*;    use unicode_script::UnicodeScript;    matches!(ch.script(), Han | Tangut | Yi)} fn is_grapheme_ideographic(text: &str) -> bool {    text.chars().any(is_char_ideographic)} fn is_grapheme_whitespace(text: &str) -> bool {    text.chars().any(|x| x.is_whitespace())} fn should_stay_with_preceding_ideograph(text: &str) -> bool {    text.chars().next().map_or(false, |ch| {        matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')    })} #[derive(PartialEq, Eq, Debug, Clone, Copy)]enum WordBreakToken<'a> {    Word { token: &'a str, grapheme_len: usize },    InlineWhitespace { token: &'a str, grapheme_len: usize },    Newline,} impl<'a> Iterator for WordBreakingTokenizer<'a> {    /// Yields a span, the count of graphemes in the token, and whether it was    /// whitespace. Note that it also breaks at word boundaries.    type Item = WordBreakToken<'a>;     fn next(&mut self) -> Option<Self::Item> {        use unicode_segmentation::UnicodeSegmentation;        if self.input.is_empty() {            return None;        }         let mut iter = self.input.graphemes(true).peekable();        let mut offset = 0;        let mut grapheme_len = 0;        if let Some(first_grapheme) = iter.next() {            let is_newline = first_grapheme == "\n";            let is_whitespace = is_grapheme_whitespace(first_grapheme);            offset += first_grapheme.len();            grapheme_len += 1;            if is_grapheme_ideographic(first_grapheme) && !is_whitespace {                if let Some(grapheme) = iter.peek().copied() {                    if should_stay_with_preceding_ideograph(grapheme) {                        offset += grapheme.len();                        grapheme_len += 1;                    }                }            } else {                let mut words = self.input[offset..].split_word_bound_indices().peekable();                let mut next_word_bound = words.peek().copied();                if next_word_bound.map_or(false, |(i, _)| i == 0) {                    next_word_bound = words.next();                }                while let Some(grapheme) = iter.peek().copied() {                    if next_word_bound.map_or(false, |(i, _)| i == offset) {                        break;                    };                    if is_grapheme_whitespace(grapheme) != is_whitespace                        || (grapheme == "\n") != is_newline                    {                        break;                    };                    offset += grapheme.len();                    grapheme_len += 1;                    iter.next();                }            }            let token = &self.input[..offset];            self.input = &self.input[offset..];            if token == "\n" {                Some(WordBreakToken::Newline)            } else if is_whitespace {                Some(WordBreakToken::InlineWhitespace {                    token,                    grapheme_len,                })            } else {                Some(WordBreakToken::Word {                    token,                    grapheme_len,                })            }        } else {            None        }    }} pub fn wrap_with_prefix(    line_prefix: String,    unwrapped_text: String,    wrap_column: usize,    tab_size: NonZeroU32,    preserve_existing_whitespace: bool,) -> String {    let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);    let mut wrapped_text = String::new();    let mut current_line = line_prefix.clone();     let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);    let mut current_line_len = line_prefix_len;    let mut in_whitespace = false;    for token in tokenizer {        let have_preceding_whitespace = in_whitespace;        match token {            WordBreakToken::Word {                token,                grapheme_len,            } => {                in_whitespace = false;                if current_line_len + grapheme_len > wrap_column                    && current_line_len != line_prefix_len                {                    wrapped_text.push_str(current_line.trim_end());                    wrapped_text.push('\n');                    current_line.truncate(line_prefix.len());                    current_line_len = line_prefix_len;                }                current_line.push_str(token);                current_line_len += grapheme_len;            }            WordBreakToken::InlineWhitespace {                mut token,                mut grapheme_len,            } => {                in_whitespace = true;                if have_preceding_whitespace && !preserve_existing_whitespace {                    continue;                }                if !preserve_existing_whitespace {                    token = " ";                    grapheme_len = 1;                }                if current_line_len + grapheme_len > wrap_column {                    wrapped_text.push_str(current_line.trim_end());                    wrapped_text.push('\n');                    current_line.truncate(line_prefix.len());                    current_line_len = line_prefix_len;                } else if current_line_len != line_prefix_len || preserve_existing_whitespace {                    current_line.push_str(token);                    current_line_len += grapheme_len;                }            }            WordBreakToken::Newline => {                in_whitespace = true;                if preserve_existing_whitespace {                    wrapped_text.push_str(current_line.trim_end());                    wrapped_text.push('\n');                    current_line.truncate(line_prefix.len());                    current_line_len = line_prefix_len;                } else if have_preceding_whitespace {                    continue;                } else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len                {                    wrapped_text.push_str(current_line.trim_end());                    wrapped_text.push('\n');                    current_line.truncate(line_prefix.len());                    current_line_len = line_prefix_len;                } else if current_line_len != line_prefix_len {                    current_line.push(' ');                    current_line_len += 1;                }            }        }    }     if !current_line.is_empty() {        wrapped_text.push_str(&current_line);    }    wrapped_text} 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)]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<()> {    load_shell_from_passwd().log_err();     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

Lines 1–500 of 1756.