terminal_hyperlinks.rs

   1use alacritty_terminal::{
   2    Term,
   3    event::EventListener,
   4    grid::Dimensions,
   5    index::{Boundary, Column, Direction as AlacDirection, Point as AlacPoint},
   6    term::{
   7        cell::Flags,
   8        search::{Match, RegexIter, RegexSearch},
   9    },
  10};
  11use log::{info, warn};
  12use regex::Regex;
  13use std::{
  14    iter::{once, once_with},
  15    ops::{Index, Range},
  16    time::{Duration, Instant},
  17};
  18use url::Url;
  19use util::paths::{PathStyle, UrlExt};
  20
  21const URL_REGEX: &str = r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^โŸจโŸฉ`']+"#;
  22const WIDE_CHAR_SPACERS: Flags =
  23    Flags::from_bits(Flags::LEADING_WIDE_CHAR_SPACER.bits() | Flags::WIDE_CHAR_SPACER.bits())
  24        .unwrap();
  25
  26pub(super) struct RegexSearches {
  27    url_regex: RegexSearch,
  28    path_hyperlink_regexes: Vec<Regex>,
  29    path_hyperlink_timeout: Duration,
  30}
  31
  32impl Default for RegexSearches {
  33    fn default() -> Self {
  34        Self {
  35            url_regex: RegexSearch::new(URL_REGEX).unwrap(),
  36            path_hyperlink_regexes: Vec::default(),
  37            path_hyperlink_timeout: Duration::default(),
  38        }
  39    }
  40}
  41impl RegexSearches {
  42    pub(super) fn new(
  43        path_hyperlink_regexes: impl IntoIterator<Item: AsRef<str>>,
  44        path_hyperlink_timeout_ms: u64,
  45    ) -> Self {
  46        Self {
  47            url_regex: RegexSearch::new(URL_REGEX).unwrap(),
  48            path_hyperlink_regexes: path_hyperlink_regexes
  49                .into_iter()
  50                .filter_map(|regex| {
  51                    Regex::new(regex.as_ref())
  52                        .inspect_err(|error| {
  53                            warn!(
  54                                concat!(
  55                                    "Ignoring path hyperlink regex specified in ",
  56                                    "`terminal.path_hyperlink_regexes`:\n\n\t{}\n\nError: {}",
  57                                ),
  58                                regex.as_ref(),
  59                                error
  60                            );
  61                        })
  62                        .ok()
  63                })
  64                .collect(),
  65            path_hyperlink_timeout: Duration::from_millis(path_hyperlink_timeout_ms),
  66        }
  67    }
  68}
  69
  70pub(super) fn find_from_grid_point<T: EventListener>(
  71    term: &Term<T>,
  72    point: AlacPoint,
  73    regex_searches: &mut RegexSearches,
  74    path_style: PathStyle,
  75) -> Option<(String, bool, Match)> {
  76    let grid = term.grid();
  77    let link = grid.index(point).hyperlink();
  78    let found_word = if let Some(ref url) = link {
  79        let mut min_index = point;
  80        loop {
  81            let new_min_index = min_index.sub(term, Boundary::Cursor, 1);
  82            if new_min_index == min_index || grid.index(new_min_index).hyperlink() != link {
  83                break;
  84            } else {
  85                min_index = new_min_index
  86            }
  87        }
  88
  89        let mut max_index = point;
  90        loop {
  91            let new_max_index = max_index.add(term, Boundary::Cursor, 1);
  92            if new_max_index == max_index || grid.index(new_max_index).hyperlink() != link {
  93                break;
  94            } else {
  95                max_index = new_max_index
  96            }
  97        }
  98
  99        let url = url.uri().to_owned();
 100        let url_match = min_index..=max_index;
 101
 102        Some((url, true, url_match))
 103    } else {
 104        let (line_start, line_end) = (term.line_search_left(point), term.line_search_right(point));
 105        if let Some((url, url_match)) = RegexIter::new(
 106            line_start,
 107            line_end,
 108            AlacDirection::Right,
 109            term,
 110            &mut regex_searches.url_regex,
 111        )
 112        .find(|rm| rm.contains(&point))
 113        .map(|url_match| {
 114            let url = term.bounds_to_string(*url_match.start(), *url_match.end());
 115            sanitize_url_punctuation(url, url_match, term)
 116        }) {
 117            Some((url, true, url_match))
 118        } else {
 119            path_match(
 120                &term,
 121                line_start,
 122                line_end,
 123                point,
 124                &mut regex_searches.path_hyperlink_regexes,
 125                regex_searches.path_hyperlink_timeout,
 126            )
 127            .map(|(path, path_match)| (path, false, path_match))
 128        }
 129    };
 130
 131    found_word.map(|(maybe_url_or_path, is_url, word_match)| {
 132        if is_url {
 133            // Treat "file://" IRIs like file paths to ensure
 134            // that line numbers at the end of the path are
 135            // handled correctly.
 136            // Use Url::to_file_path() to properly handle Windows drive letters
 137            // (e.g., file:///C:/path -> C:\path)
 138            if maybe_url_or_path.starts_with("file://") {
 139                if let Ok(url) = Url::parse(&maybe_url_or_path) {
 140                    if let Ok(path) = url.to_file_path_ext(path_style) {
 141                        return (path.to_string_lossy().into_owned(), false, word_match);
 142                    }
 143                }
 144                // Fallback: strip file:// prefix if URL parsing fails
 145                let path = maybe_url_or_path
 146                    .strip_prefix("file://")
 147                    .unwrap_or(&maybe_url_or_path);
 148                (path.to_string(), false, word_match)
 149            } else {
 150                (maybe_url_or_path, true, word_match)
 151            }
 152        } else {
 153            (maybe_url_or_path, false, word_match)
 154        }
 155    })
 156}
 157
 158fn sanitize_url_punctuation<T: EventListener>(
 159    url: String,
 160    url_match: Match,
 161    term: &Term<T>,
 162) -> (String, Match) {
 163    let mut sanitized_url = url;
 164    let mut chars_trimmed = 0;
 165
 166    // Count parentheses in the URL
 167    let (open_parens, mut close_parens) =
 168        sanitized_url
 169            .chars()
 170            .fold((0, 0), |(opens, closes), c| match c {
 171                '(' => (opens + 1, closes),
 172                ')' => (opens, closes + 1),
 173                _ => (opens, closes),
 174            });
 175
 176    // Remove trailing characters that shouldn't be at the end of URLs
 177    while let Some(last_char) = sanitized_url.chars().last() {
 178        let should_remove = match last_char {
 179            // These may be part of a URL but not at the end. It's not that the spec
 180            // doesn't allow them, but they are frequently used in plain text as delimiters
 181            // where they're not meant to be part of the URL.
 182            '.' | ',' | ':' | ';' => true,
 183            '(' => true,
 184            ')' if close_parens > open_parens => {
 185                close_parens -= 1;
 186
 187                true
 188            }
 189            _ => false,
 190        };
 191
 192        if should_remove {
 193            sanitized_url.pop();
 194            chars_trimmed += 1;
 195        } else {
 196            break;
 197        }
 198    }
 199
 200    if chars_trimmed > 0 {
 201        let new_end = url_match.end().sub(term, Boundary::Grid, chars_trimmed);
 202        let sanitized_match = Match::new(*url_match.start(), new_end);
 203        (sanitized_url, sanitized_match)
 204    } else {
 205        (sanitized_url, url_match)
 206    }
 207}
 208
 209fn path_match<T>(
 210    term: &Term<T>,
 211    line_start: AlacPoint,
 212    line_end: AlacPoint,
 213    hovered: AlacPoint,
 214    path_hyperlink_regexes: &mut Vec<Regex>,
 215    path_hyperlink_timeout: Duration,
 216) -> Option<(String, Match)> {
 217    if path_hyperlink_regexes.is_empty() || path_hyperlink_timeout.as_millis() == 0 {
 218        return None;
 219    }
 220    debug_assert!(line_start <= hovered);
 221    debug_assert!(line_end >= hovered);
 222    let search_start_time = Instant::now();
 223
 224    let timed_out = || {
 225        let elapsed_time = Instant::now().saturating_duration_since(search_start_time);
 226        (elapsed_time > path_hyperlink_timeout)
 227            .then_some((elapsed_time.as_millis(), path_hyperlink_timeout.as_millis()))
 228    };
 229
 230    // This used to be: `let line = term.bounds_to_string(line_start, line_end)`, however, that
 231    // api compresses tab characters into a single space, whereas we require a cell accurate
 232    // string representation of the line. The below algorithm does this, but seems a bit odd.
 233    // Maybe there is a clean api for doing this, but I couldn't find it.
 234    let mut line = String::with_capacity(
 235        (line_end.line.0 - line_start.line.0 + 1) as usize * term.grid().columns(),
 236    );
 237    let first_cell = &term.grid()[line_start];
 238    let mut prev_len = 0;
 239    line.push(first_cell.c);
 240    let mut prev_char_is_space = first_cell.c == ' ';
 241    let mut hovered_point_byte_offset = None;
 242    let mut hovered_word_start_offset = None;
 243    let mut hovered_word_end_offset = None;
 244
 245    if line_start == hovered {
 246        hovered_point_byte_offset = Some(0);
 247        if first_cell.c != ' ' {
 248            hovered_word_start_offset = Some(0);
 249        }
 250    }
 251
 252    for cell in term.grid().iter_from(line_start) {
 253        if cell.point > line_end {
 254            break;
 255        }
 256
 257        if !cell.flags.intersects(WIDE_CHAR_SPACERS) {
 258            prev_len = line.len();
 259            match cell.c {
 260                ' ' | '\t' => {
 261                    if hovered_point_byte_offset.is_some() && !prev_char_is_space {
 262                        if hovered_word_end_offset.is_none() {
 263                            hovered_word_end_offset = Some(line.len());
 264                        }
 265                    }
 266                    line.push(' ');
 267                    prev_char_is_space = true;
 268                }
 269                c @ _ => {
 270                    if hovered_point_byte_offset.is_none() && prev_char_is_space {
 271                        hovered_word_start_offset = Some(line.len());
 272                    }
 273                    line.push(c);
 274                    prev_char_is_space = false;
 275                }
 276            }
 277        }
 278
 279        if cell.point == hovered {
 280            debug_assert!(hovered_point_byte_offset.is_none());
 281            hovered_point_byte_offset = Some(prev_len);
 282        }
 283    }
 284    let line = line.trim_ascii_end();
 285    let hovered_point_byte_offset = hovered_point_byte_offset?;
 286    let hovered_word_range = {
 287        let word_start_offset = hovered_word_start_offset.unwrap_or(0);
 288        (word_start_offset != 0)
 289            .then_some(word_start_offset..hovered_word_end_offset.unwrap_or(line.len()))
 290    };
 291    if line.len() <= hovered_point_byte_offset {
 292        return None;
 293    }
 294    let found_from_range = |path_range: Range<usize>,
 295                            link_range: Range<usize>,
 296                            position: Option<(u32, Option<u32>)>| {
 297        let advance_point_by_str = |mut point: AlacPoint, s: &str| {
 298            for _ in s.chars() {
 299                point = term
 300                    .expand_wide(point, AlacDirection::Right)
 301                    .add(term, Boundary::Grid, 1);
 302            }
 303
 304            // There does not appear to be an alacritty api that is
 305            // "move to start of current wide char", so we have to do it ourselves.
 306            let flags = term.grid().index(point).flags;
 307            if flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) {
 308                AlacPoint::new(point.line + 1, Column(0))
 309            } else if flags.contains(Flags::WIDE_CHAR_SPACER) {
 310                AlacPoint::new(point.line, point.column - 1)
 311            } else {
 312                point
 313            }
 314        };
 315
 316        let link_start = advance_point_by_str(line_start, &line[..link_range.start]);
 317        let link_end = advance_point_by_str(link_start, &line[link_range]);
 318        let link_match = link_start
 319            ..=term
 320                .expand_wide(link_end, AlacDirection::Left)
 321                .sub(term, Boundary::Grid, 1);
 322
 323        (
 324            {
 325                let mut path = line[path_range].to_string();
 326                position.inspect(|(line, column)| {
 327                    path += &format!(":{line}");
 328                    column.inspect(|column| path += &format!(":{column}"));
 329                });
 330                path
 331            },
 332            link_match,
 333        )
 334    };
 335
 336    for regex in path_hyperlink_regexes {
 337        let mut path_found = false;
 338
 339        for (line_start_offset, captures) in once(
 340            regex
 341                .captures_iter(&line)
 342                .next()
 343                .map(|captures| (0, captures)),
 344        )
 345        .chain(once_with(|| {
 346            if let Some(hovered_word_range) = &hovered_word_range {
 347                regex
 348                    .captures_iter(&line[hovered_word_range.clone()])
 349                    .next()
 350                    .map(|captures| (hovered_word_range.start, captures))
 351            } else {
 352                None
 353            }
 354        }))
 355        .flatten()
 356        {
 357            path_found = true;
 358            let match_range = captures.get(0).unwrap().range();
 359            let (mut path_range, line_column) = if let Some(path) = captures.name("path") {
 360                let parse = |name: &str| {
 361                    captures
 362                        .name(name)
 363                        .and_then(|capture| capture.as_str().parse().ok())
 364                };
 365
 366                (
 367                    path.range(),
 368                    parse("line").map(|line| (line, parse("column"))),
 369                )
 370            } else {
 371                (match_range.clone(), None)
 372            };
 373            let mut link_range = captures
 374                .name("link")
 375                .map_or_else(|| match_range.clone(), |link| link.range());
 376
 377            path_range.start += line_start_offset;
 378            path_range.end += line_start_offset;
 379            link_range.start += line_start_offset;
 380            link_range.end += line_start_offset;
 381
 382            if !link_range.contains(&hovered_point_byte_offset) {
 383                // No match, just skip.
 384                continue;
 385            }
 386            let found = found_from_range(path_range, link_range, line_column);
 387
 388            if found.1.contains(&hovered) {
 389                return Some(found);
 390            }
 391        }
 392
 393        if path_found {
 394            return None;
 395        }
 396
 397        if let Some((timed_out_ms, timeout_ms)) = timed_out() {
 398            warn!("Timed out processing path hyperlink regexes after {timed_out_ms}ms");
 399            info!("{timeout_ms}ms time out specified in `terminal.path_hyperlink_timeout_ms`");
 400            return None;
 401        }
 402    }
 403
 404    None
 405}
 406
 407#[cfg(test)]
 408mod tests {
 409    use crate::terminal_settings::TerminalSettings;
 410
 411    use super::*;
 412    use alacritty_terminal::{
 413        event::VoidListener,
 414        grid::Dimensions,
 415        index::{Boundary, Column, Line, Point as AlacPoint},
 416        term::{Config, cell::Flags, test::TermSize},
 417        vte::ansi::Handler,
 418    };
 419    use regex::Regex;
 420    use settings::{self, Settings, SettingsContent};
 421    use std::{cell::RefCell, ops::RangeInclusive, path::PathBuf, rc::Rc};
 422    use url::Url;
 423    use util::paths::PathWithPosition;
 424
 425    fn re_test(re: &str, hay: &str, expected: Vec<&str>) {
 426        let results: Vec<_> = Regex::new(re)
 427            .unwrap()
 428            .find_iter(hay)
 429            .map(|m| m.as_str())
 430            .collect();
 431        assert_eq!(results, expected);
 432    }
 433
 434    #[test]
 435    fn test_url_regex() {
 436        re_test(
 437            URL_REGEX,
 438            "test http://example.com test 'https://website1.com' test mailto:bob@example.com train",
 439            vec![
 440                "http://example.com",
 441                "https://website1.com",
 442                "mailto:bob@example.com",
 443            ],
 444        );
 445    }
 446
 447    #[test]
 448    fn test_url_parentheses_sanitization() {
 449        // Test our sanitize_url_parentheses function directly
 450        let test_cases = vec![
 451            // Cases that should be sanitized (unbalanced parentheses)
 452            ("https://www.google.com/)", "https://www.google.com/"),
 453            ("https://example.com/path)", "https://example.com/path"),
 454            ("https://test.com/))", "https://test.com/"),
 455            ("https://test.com/(((", "https://test.com/"),
 456            ("https://test.com/(test)(", "https://test.com/(test)"),
 457            // Cases that should NOT be sanitized (balanced parentheses)
 458            (
 459                "https://en.wikipedia.org/wiki/Example_(disambiguation)",
 460                "https://en.wikipedia.org/wiki/Example_(disambiguation)",
 461            ),
 462            ("https://test.com/(hello)", "https://test.com/(hello)"),
 463            (
 464                "https://example.com/path(1)(2)",
 465                "https://example.com/path(1)(2)",
 466            ),
 467            // Edge cases
 468            ("https://test.com/", "https://test.com/"),
 469            ("https://example.com", "https://example.com"),
 470        ];
 471
 472        for (input, expected) in test_cases {
 473            // Create a minimal terminal for testing
 474            let term = Term::new(Config::default(), &TermSize::new(80, 24), VoidListener);
 475
 476            // Create a dummy match that spans the entire input
 477            let start_point = AlacPoint::new(Line(0), Column(0));
 478            let end_point = AlacPoint::new(Line(0), Column(input.len()));
 479            let dummy_match = Match::new(start_point, end_point);
 480
 481            let (result, _) = sanitize_url_punctuation(input.to_string(), dummy_match, &term);
 482            assert_eq!(result, expected, "Failed for input: {}", input);
 483        }
 484    }
 485
 486    #[test]
 487    fn test_url_punctuation_sanitization() {
 488        // Test URLs with trailing punctuation (sentence/text punctuation)
 489        // The sanitize_url_punctuation function removes ., ,, :, ;, from the end
 490        let test_cases = vec![
 491            ("https://example.com.", "https://example.com"),
 492            (
 493                "https://github.com/zed-industries/zed.",
 494                "https://github.com/zed-industries/zed",
 495            ),
 496            (
 497                "https://example.com/path/file.html.",
 498                "https://example.com/path/file.html",
 499            ),
 500            (
 501                "https://example.com/file.pdf.",
 502                "https://example.com/file.pdf",
 503            ),
 504            ("https://example.com:8080.", "https://example.com:8080"),
 505            ("https://example.com..", "https://example.com"),
 506            (
 507                "https://en.wikipedia.org/wiki/C.E.O.",
 508                "https://en.wikipedia.org/wiki/C.E.O",
 509            ),
 510            ("https://example.com,", "https://example.com"),
 511            ("https://example.com/path,", "https://example.com/path"),
 512            ("https://example.com,,", "https://example.com"),
 513            ("https://example.com:", "https://example.com"),
 514            ("https://example.com/path:", "https://example.com/path"),
 515            ("https://example.com::", "https://example.com"),
 516            ("https://example.com;", "https://example.com"),
 517            ("https://example.com/path;", "https://example.com/path"),
 518            ("https://example.com;;", "https://example.com"),
 519            ("https://example.com.,", "https://example.com"),
 520            ("https://example.com.:;", "https://example.com"),
 521            ("https://example.com!.", "https://example.com!"),
 522            ("https://example.com/).", "https://example.com/"),
 523            ("https://example.com/);", "https://example.com/"),
 524            ("https://example.com/;)", "https://example.com/"),
 525            (
 526                "https://example.com/v1.0/api",
 527                "https://example.com/v1.0/api",
 528            ),
 529            ("https://192.168.1.1", "https://192.168.1.1"),
 530            ("https://sub.domain.com", "https://sub.domain.com"),
 531            (
 532                "https://example.com?query=value",
 533                "https://example.com?query=value",
 534            ),
 535            ("https://example.com?a=1&b=2", "https://example.com?a=1&b=2"),
 536            (
 537                "https://example.com/path:8080",
 538                "https://example.com/path:8080",
 539            ),
 540        ];
 541
 542        for (input, expected) in test_cases {
 543            // Create a minimal terminal for testing
 544            let term = Term::new(Config::default(), &TermSize::new(80, 24), VoidListener);
 545
 546            // Create a dummy match that spans the entire input
 547            let start_point = AlacPoint::new(Line(0), Column(0));
 548            let end_point = AlacPoint::new(Line(0), Column(input.len()));
 549            let dummy_match = Match::new(start_point, end_point);
 550
 551            let (result, _) = sanitize_url_punctuation(input.to_string(), dummy_match, &term);
 552            assert_eq!(result, expected, "Failed for input: {}", input);
 553        }
 554    }
 555
 556    macro_rules! test_hyperlink {
 557        ($($lines:expr),+; $hyperlink_kind:ident) => { {
 558            use crate::terminal_hyperlinks::tests::line_cells_count;
 559            use std::cmp;
 560
 561            let test_lines = vec![$($lines),+];
 562            let (total_cells, longest_line_cells) =
 563                test_lines.iter().copied()
 564                    .map(line_cells_count)
 565                    .fold((0, 0), |state, cells| (state.0 + cells, cmp::max(state.1, cells)));
 566            let contains_tab_char = test_lines.iter().copied()
 567                .map(str::chars).flatten().find(|&c| c == '\t');
 568            let columns = if contains_tab_char.is_some() {
 569                // This avoids tabs at end of lines causing whitespace-eating line wraps...
 570                vec![longest_line_cells + 1]
 571            } else {
 572                // Alacritty has issues with 2 columns, use 3 as the minimum for now.
 573                vec![3, longest_line_cells / 2, longest_line_cells + 1]
 574            };
 575            test_hyperlink!(
 576                columns;
 577                total_cells;
 578                test_lines.iter().copied();
 579                $hyperlink_kind
 580            )
 581        } };
 582
 583        ($columns:expr; $total_cells:expr; $lines:expr; $hyperlink_kind:ident) => { {
 584            use crate::terminal_hyperlinks::tests::{ test_hyperlink, HyperlinkKind };
 585
 586            let source_location = format!("{}:{}", std::file!(), std::line!());
 587            for columns in $columns {
 588                test_hyperlink(columns, $total_cells, $lines, HyperlinkKind::$hyperlink_kind,
 589                    &source_location);
 590            }
 591        } };
 592    }
 593
 594    mod path {
 595        /// ๐Ÿ‘‰ := **hovered** on following char
 596        ///
 597        /// ๐Ÿ‘ˆ := **hovered** on wide char spacer of previous full width char
 598        ///
 599        /// **`โ€นโ€บ`** := expected **hyperlink** match
 600        ///
 601        /// **`ยซยป`** := expected **path**, **row**, and **column** capture groups
 602        ///
 603        /// [**`cโ‚€, cโ‚, โ€ฆ, cโ‚™;`**]โ‚’โ‚šโ‚œ := use specified terminal widths of `cโ‚€, cโ‚, โ€ฆ, cโ‚™` **columns**
 604        /// (defaults to `3, longest_line_cells / 2, longest_line_cells + 1;`)
 605        ///
 606        macro_rules! test_path {
 607            ($($lines:literal),+) => { test_hyperlink!($($lines),+; Path) };
 608        }
 609
 610        #[test]
 611        fn simple() {
 612            // Rust paths
 613            // Just the path
 614            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยปโ€บ");
 615            test_path!("โ€นยซ/test/cool๐Ÿ‘‰.rsยปโ€บ");
 616
 617            // path and line
 618            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยปโ€บ");
 619            test_path!("โ€นยซ/test/cool.rsยป๐Ÿ‘‰:ยซ4ยปโ€บ");
 620            test_path!("โ€นยซ/test/cool.rsยป:ยซ๐Ÿ‘‰4ยปโ€บ");
 621            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป(ยซ4ยป)โ€บ");
 622            test_path!("โ€นยซ/test/cool.rsยป๐Ÿ‘‰(ยซ4ยป)โ€บ");
 623            test_path!("โ€นยซ/test/cool.rsยป(ยซ๐Ÿ‘‰4ยป)โ€บ");
 624            test_path!("โ€นยซ/test/cool.rsยป(ยซ4ยป๐Ÿ‘‰)โ€บ");
 625
 626            // path, line, and column
 627            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ");
 628            test_path!("โ€นยซ/test/cool.rsยป:ยซ4ยป:ยซ๐Ÿ‘‰2ยปโ€บ");
 629            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป(ยซ4ยป,ยซ2ยป)โ€บ");
 630            test_path!("โ€นยซ/test/cool.rsยป(ยซ4ยป๐Ÿ‘‰,ยซ2ยป)โ€บ");
 631
 632            // path, line, column, and ':' suffix
 633            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ:");
 634            test_path!("โ€นยซ/test/cool.rsยป:ยซ4ยป:ยซ๐Ÿ‘‰2ยปโ€บ:");
 635            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป(ยซ4ยป,ยซ2ยป)โ€บ:");
 636            test_path!("โ€นยซ/test/cool.rsยป(ยซ4ยป,ยซ2ยป๐Ÿ‘‰)โ€บ:");
 637            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป:(ยซ4ยป,ยซ2ยป)โ€บ:");
 638            test_path!("โ€นยซ/test/cool.rsยป:(ยซ4ยป,ยซ2ยป๐Ÿ‘‰)โ€บ:");
 639            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป:(ยซ4ยป:ยซ2ยป)โ€บ:");
 640            test_path!("โ€นยซ/test/cool.rsยป:(ยซ4ยป:ยซ2ยป๐Ÿ‘‰)โ€บ:");
 641            test_path!("/test/cool.rs:4:2๐Ÿ‘‰:", "What is this?");
 642            test_path!("/test/cool.rs(4,2)๐Ÿ‘‰:", "What is this?");
 643
 644            // path, line, column, and description
 645            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ:Error!");
 646            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป,ยซ2ยป)โ€บ:Error!");
 647
 648            // Cargo output
 649            test_path!("    Compiling Cool ๐Ÿ‘‰(/test/Cool)");
 650            test_path!("    Compiling Cool (โ€นยซ/๐Ÿ‘‰test/Coolยปโ€บ)");
 651            test_path!("    Compiling Cool (/test/Cool๐Ÿ‘‰)");
 652
 653            // Python
 654            test_path!("โ€นยซawe๐Ÿ‘‰some.pyยปโ€บ");
 655            test_path!("โ€นยซ๐Ÿ‘‰aยปโ€บ ");
 656
 657            test_path!("    โ€นF๐Ÿ‘‰ile \"ยซ/awesome.pyยป\", line ยซ42ยปโ€บ: Wat?");
 658            test_path!("    โ€นFile \"ยซ/awe๐Ÿ‘‰some.pyยป\", line ยซ42ยปโ€บ");
 659            test_path!("    โ€นFile \"ยซ/awesome.pyยป๐Ÿ‘‰\", line ยซ42ยปโ€บ: Wat?");
 660            test_path!("    โ€นFile \"ยซ/awesome.pyยป\", line ยซ4๐Ÿ‘‰2ยปโ€บ");
 661        }
 662
 663        #[test]
 664        fn simple_with_descriptions() {
 665            // path, line, column and description
 666            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ:ไพ‹Descไพ‹ไพ‹ไพ‹");
 667            test_path!("โ€นยซ/test/cool.rsยป:ยซ4ยป:ยซ๐Ÿ‘‰2ยปโ€บ:ไพ‹Descไพ‹ไพ‹ไพ‹");
 668            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป(ยซ4ยป,ยซ2ยป)โ€บ:ไพ‹Descไพ‹ไพ‹ไพ‹");
 669            test_path!("โ€นยซ/test/cool.rsยป(ยซ4ยป๐Ÿ‘‰,ยซ2ยป)โ€บ:ไพ‹Descไพ‹ไพ‹ไพ‹");
 670
 671            // path, line, column and description w/extra colons
 672            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ::ไพ‹Descไพ‹ไพ‹ไพ‹");
 673            test_path!("โ€นยซ/test/cool.rsยป:ยซ4ยป:ยซ๐Ÿ‘‰2ยปโ€บ::ไพ‹Descไพ‹ไพ‹ไพ‹");
 674            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป(ยซ4ยป,ยซ2ยป)โ€บ::ไพ‹Descไพ‹ไพ‹ไพ‹");
 675            test_path!("โ€นยซ/test/cool.rsยป(ยซ4ยป,ยซ2ยป๐Ÿ‘‰)โ€บ::ไพ‹Descไพ‹ไพ‹ไพ‹");
 676        }
 677
 678        #[test]
 679        fn multiple_same_line() {
 680            test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยปโ€บ /test/cool.rs");
 681            test_path!("/test/cool.rs โ€นยซ/๐Ÿ‘‰test/cool.rsยปโ€บ");
 682
 683            test_path!(
 684                "โ€นยซ๐Ÿฆ€ multiple_๐Ÿ‘‰same_line ๐Ÿฆ€ยป ๐Ÿšฃยซ4ยป ๐Ÿ›๏ธยซ2ยปโ€บ: ๐Ÿฆ€ multiple_same_line ๐Ÿฆ€ ๐Ÿšฃ4 ๐Ÿ›๏ธ2:"
 685            );
 686
 687            // ls output (tab separated)
 688            test_path!(
 689                "โ€นยซCarg๐Ÿ‘‰o.tomlยปโ€บ\t\texperiments\t\tnotebooks\t\trust-toolchain.toml\ttooling"
 690            );
 691            test_path!(
 692                "Cargo.toml\t\tโ€นยซexper๐Ÿ‘‰imentsยปโ€บ\t\tnotebooks\t\trust-toolchain.toml\ttooling"
 693            );
 694            test_path!(
 695                "Cargo.toml\t\texperiments\t\tโ€นยซnote๐Ÿ‘‰booksยปโ€บ\t\trust-toolchain.toml\ttooling"
 696            );
 697            test_path!(
 698                "Cargo.toml\t\texperiments\t\tnotebooks\t\tโ€นยซrust-t๐Ÿ‘‰oolchain.tomlยปโ€บ\ttooling"
 699            );
 700            test_path!(
 701                "Cargo.toml\t\texperiments\t\tnotebooks\t\trust-toolchain.toml\tโ€นยซtoo๐Ÿ‘‰lingยปโ€บ"
 702            );
 703        }
 704
 705        #[test]
 706        fn colons_galore() {
 707            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ");
 708            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ:");
 709            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ");
 710            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ:");
 711            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ1ยป)โ€บ");
 712            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ1ยป)โ€บ:");
 713            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ1ยป,ยซ618ยป)โ€บ");
 714            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ1ยป,ยซ618ยป)โ€บ:");
 715            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป::ยซ42ยปโ€บ");
 716            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป::ยซ42ยปโ€บ:");
 717            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ1ยป,ยซ618ยป)โ€บ::");
 718        }
 719
 720        #[test]
 721        fn quotes_and_brackets() {
 722            test_path!("\"โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ\"");
 723            test_path!("'โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ'");
 724            test_path!("`โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ`");
 725
 726            test_path!("[โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ]");
 727            test_path!("(โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ)");
 728            test_path!("{โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ}");
 729            test_path!("<โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ>");
 730
 731            test_path!("[\"โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ\"]");
 732            test_path!("'(โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ)'");
 733
 734            test_path!("\"โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ\"");
 735            test_path!("'โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ'");
 736            test_path!("`โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ`");
 737
 738            test_path!("[โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ]");
 739            test_path!("(โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ)");
 740            test_path!("{โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ}");
 741            test_path!("<โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ>");
 742
 743            test_path!("[\"โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยป:ยซ2ยปโ€บ\"]");
 744
 745            test_path!("\"โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป)โ€บ\"");
 746            test_path!("'โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป)โ€บ'");
 747            test_path!("`โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป)โ€บ`");
 748
 749            test_path!("[โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป)โ€บ]");
 750            test_path!("(โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป)โ€บ)");
 751            test_path!("{โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป)โ€บ}");
 752            test_path!("<โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป)โ€บ>");
 753
 754            test_path!("[\"โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป)โ€บ\"]");
 755
 756            test_path!("\"โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป,ยซ2ยป)โ€บ\"");
 757            test_path!("'โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป,ยซ2ยป)โ€บ'");
 758            test_path!("`โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป,ยซ2ยป)โ€บ`");
 759
 760            test_path!("[โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป,ยซ2ยป)โ€บ]");
 761            test_path!("(โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป,ยซ2ยป)โ€บ)");
 762            test_path!("{โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป,ยซ2ยป)โ€บ}");
 763            test_path!("<โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป,ยซ2ยป)โ€บ>");
 764
 765            test_path!("[\"โ€นยซ/test/co๐Ÿ‘‰ol.rsยป(ยซ4ยป,ยซ2ยป)โ€บ\"]");
 766
 767            // Imbalanced
 768            test_path!("([โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ] was here...)");
 769            test_path!("[Here's <โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ>]");
 770            test_path!("('โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ' was here...)");
 771            test_path!("[Here's `โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ`]");
 772        }
 773
 774        #[test]
 775        fn trailing_punctuation() {
 776            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยปโ€บ:,..");
 777            test_path!("/test/cool.rs:,๐Ÿ‘‰..");
 778            test_path!("โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ:,");
 779            test_path!("/test/cool.rs:4:๐Ÿ‘‰,");
 780            test_path!("[\"โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ\"]:,");
 781            test_path!("'(โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ),,'...");
 782            test_path!("('โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ'::: was here...)");
 783            test_path!("[Here's <โ€นยซ/test/co๐Ÿ‘‰ol.rsยป:ยซ4ยปโ€บ>]::: ");
 784        }
 785
 786        #[test]
 787        fn word_wide_chars() {
 788            // Rust paths
 789            test_path!("โ€นยซ/๐Ÿ‘‰ไพ‹/cool.rsยปโ€บ");
 790            test_path!("โ€นยซ/ไพ‹๐Ÿ‘ˆ/cool.rsยปโ€บ");
 791            test_path!("โ€นยซ/ไพ‹/cool.rsยป:ยซ๐Ÿ‘‰4ยปโ€บ");
 792            test_path!("โ€นยซ/ไพ‹/cool.rsยป:ยซ4ยป:ยซ๐Ÿ‘‰2ยปโ€บ");
 793
 794            // Cargo output
 795            test_path!("    Compiling Cool (โ€นยซ/๐Ÿ‘‰ไพ‹/Coolยปโ€บ)");
 796            test_path!("    Compiling Cool (โ€นยซ/ไพ‹๐Ÿ‘ˆ/Coolยปโ€บ)");
 797
 798            test_path!("    Compiling Cool (โ€นยซ/๐Ÿ‘‰ไพ‹/Cool Spacesยปโ€บ)");
 799            test_path!("    Compiling Cool (โ€นยซ/ไพ‹๐Ÿ‘ˆ/Cool Spacesยปโ€บ)");
 800            test_path!("    Compiling Cool (โ€นยซ/๐Ÿ‘‰ไพ‹/Cool Spacesยป:ยซ4ยป:ยซ2ยปโ€บ)");
 801            test_path!("    Compiling Cool (โ€นยซ/ไพ‹๐Ÿ‘ˆ/Cool Spacesยป(ยซ4ยป,ยซ2ยป)โ€บ)");
 802
 803            test_path!("    --> โ€นยซ/๐Ÿ‘‰ไพ‹/Cool Spacesยปโ€บ");
 804            test_path!("    ::: โ€นยซ/ไพ‹๐Ÿ‘ˆ/Cool Spacesยปโ€บ");
 805            test_path!("    --> โ€นยซ/๐Ÿ‘‰ไพ‹/Cool Spacesยป:ยซ4ยป:ยซ2ยปโ€บ");
 806            test_path!("    ::: โ€นยซ/ไพ‹๐Ÿ‘ˆ/Cool Spacesยป(ยซ4ยป,ยซ2ยป)โ€บ");
 807            test_path!("    panicked at โ€นยซ/๐Ÿ‘‰ไพ‹/Cool Spacesยป:ยซ4ยป:ยซ2ยปโ€บ:");
 808            test_path!("    panicked at โ€นยซ/ไพ‹๐Ÿ‘ˆ/Cool Spacesยป(ยซ4ยป,ยซ2ยป)โ€บ:");
 809            test_path!("    at โ€นยซ/๐Ÿ‘‰ไพ‹/Cool Spacesยป:ยซ4ยป:ยซ2ยปโ€บ");
 810            test_path!("    at โ€นยซ/ไพ‹๐Ÿ‘ˆ/Cool Spacesยป(ยซ4ยป,ยซ2ยป)โ€บ");
 811
 812            // Python
 813            test_path!("โ€นยซ๐Ÿ‘‰ไพ‹wesome.pyยปโ€บ");
 814            test_path!("โ€นยซไพ‹๐Ÿ‘ˆwesome.pyยปโ€บ");
 815            test_path!("    โ€นFile \"ยซ/๐Ÿ‘‰ไพ‹wesome.pyยป\", line ยซ42ยปโ€บ: Wat?");
 816            test_path!("    โ€นFile \"ยซ/ไพ‹๐Ÿ‘ˆwesome.pyยป\", line ยซ42ยปโ€บ: Wat?");
 817        }
 818
 819        #[test]
 820        fn non_word_wide_chars() {
 821            // Mojo diagnostic message
 822            test_path!("    โ€นFile \"ยซ/awe๐Ÿ‘‰some.๐Ÿ”ฅยป\", line ยซ42ยปโ€บ: Wat?");
 823            test_path!("    โ€นFile \"ยซ/awesome๐Ÿ‘‰.๐Ÿ”ฅยป\", line ยซ42ยปโ€บ: Wat?");
 824            test_path!("    โ€นFile \"ยซ/awesome.๐Ÿ‘‰๐Ÿ”ฅยป\", line ยซ42ยปโ€บ: Wat?");
 825            test_path!("    โ€นFile \"ยซ/awesome.๐Ÿ”ฅ๐Ÿ‘ˆยป\", line ยซ42ยปโ€บ: Wat?");
 826        }
 827
 828        /// These likely rise to the level of being worth fixing.
 829        mod issues {
 830            #[test]
 831            // <https://github.com/alacritty/alacritty/issues/8586>
 832            fn issue_alacritty_8586() {
 833                // Rust paths
 834                test_path!("โ€นยซ/๐Ÿ‘‰ไพ‹/cool.rsยปโ€บ");
 835                test_path!("โ€นยซ/ไพ‹๐Ÿ‘ˆ/cool.rsยปโ€บ");
 836                test_path!("โ€นยซ/ไพ‹/cool.rsยป:ยซ๐Ÿ‘‰4ยปโ€บ");
 837                test_path!("โ€นยซ/ไพ‹/cool.rsยป:ยซ4ยป:ยซ๐Ÿ‘‰2ยปโ€บ");
 838
 839                // Cargo output
 840                test_path!("    Compiling Cool (โ€นยซ/๐Ÿ‘‰ไพ‹/Coolยปโ€บ)");
 841                test_path!("    Compiling Cool (โ€นยซ/ไพ‹๐Ÿ‘ˆ/Coolยปโ€บ)");
 842
 843                // Python
 844                test_path!("โ€นยซ๐Ÿ‘‰ไพ‹wesome.pyยปโ€บ");
 845                test_path!("โ€นยซไพ‹๐Ÿ‘ˆwesome.pyยปโ€บ");
 846                test_path!("    โ€นFile \"ยซ/๐Ÿ‘‰ไพ‹wesome.pyยป\", line ยซ42ยปโ€บ: Wat?");
 847                test_path!("    โ€นFile \"ยซ/ไพ‹๐Ÿ‘ˆwesome.pyยป\", line ยซ42ยปโ€บ: Wat?");
 848            }
 849
 850            #[test]
 851            // <https://github.com/zed-industries/zed/issues/12338>
 852            fn issue_12338_regex() {
 853                // Issue #12338
 854                test_path!(".rw-r--r--     0     staff 05-27 14:03 โ€นยซ'test file ๐Ÿ‘‰1.txt'ยปโ€บ");
 855                test_path!(".rw-r--r--     0     staff 05-27 14:03 โ€นยซ๐Ÿ‘‰'test file 1.txt'ยปโ€บ");
 856            }
 857
 858            #[test]
 859            // <https://github.com/zed-industries/zed/issues/12338>
 860            fn issue_12338() {
 861                // Issue #12338
 862                test_path!(".rw-r--r--     0     staff 05-27 14:03 โ€นยซtest๐Ÿ‘‰ใ€2.txtยปโ€บ");
 863                test_path!(".rw-r--r--     0     staff 05-27 14:03 โ€นยซtestใ€๐Ÿ‘ˆ2.txtยปโ€บ");
 864                test_path!(".rw-r--r--     0     staff 05-27 14:03 โ€นยซtest๐Ÿ‘‰ใ€‚3.txtยปโ€บ");
 865                test_path!(".rw-r--r--     0     staff 05-27 14:03 โ€นยซtestใ€‚๐Ÿ‘ˆ3.txtยปโ€บ");
 866
 867                // Rust paths
 868                test_path!("โ€นยซ/๐Ÿ‘‰๐Ÿƒ/๐Ÿฆ€.rsยปโ€บ");
 869                test_path!("โ€นยซ/๐Ÿƒ๐Ÿ‘ˆ/๐Ÿฆ€.rsยปโ€บ");
 870                test_path!("โ€นยซ/๐Ÿƒ/๐Ÿ‘‰๐Ÿฆ€.rsยป:ยซ4ยปโ€บ");
 871                test_path!("โ€นยซ/๐Ÿƒ/๐Ÿฆ€๐Ÿ‘ˆ.rsยป:ยซ4ยป:ยซ2ยปโ€บ");
 872
 873                // Cargo output
 874                test_path!("    Compiling Cool (โ€นยซ/๐Ÿ‘‰๐Ÿƒ/Coolยปโ€บ)");
 875                test_path!("    Compiling Cool (โ€นยซ/๐Ÿƒ๐Ÿ‘ˆ/Coolยปโ€บ)");
 876
 877                // Python
 878                test_path!("โ€นยซ๐Ÿ‘‰๐Ÿƒwesome.pyยปโ€บ");
 879                test_path!("โ€นยซ๐Ÿƒ๐Ÿ‘ˆwesome.pyยปโ€บ");
 880                test_path!("    โ€นFile \"ยซ/๐Ÿ‘‰๐Ÿƒwesome.pyยป\", line ยซ42ยปโ€บ: Wat?");
 881                test_path!("    โ€นFile \"ยซ/๐Ÿƒ๐Ÿ‘ˆwesome.pyยป\", line ยซ42ยปโ€บ: Wat?");
 882
 883                // Mojo
 884                test_path!("โ€นยซ/awe๐Ÿ‘‰some.๐Ÿ”ฅยปโ€บ is some good Mojo!");
 885                test_path!("โ€นยซ/awesome๐Ÿ‘‰.๐Ÿ”ฅยปโ€บ is some good Mojo!");
 886                test_path!("โ€นยซ/awesome.๐Ÿ‘‰๐Ÿ”ฅยปโ€บ is some good Mojo!");
 887                test_path!("โ€นยซ/awesome.๐Ÿ”ฅ๐Ÿ‘ˆยปโ€บ is some good Mojo!");
 888                test_path!("    โ€นFile \"ยซ/๐Ÿ‘‰๐Ÿƒwesome.๐Ÿ”ฅยป\", line ยซ42ยปโ€บ: Wat?");
 889                test_path!("    โ€นFile \"ยซ/๐Ÿƒ๐Ÿ‘ˆwesome.๐Ÿ”ฅยป\", line ยซ42ยปโ€บ: Wat?");
 890            }
 891
 892            #[test]
 893            // <https://github.com/zed-industries/zed/issues/40202>
 894            fn issue_40202() {
 895                // Elixir
 896                test_path!("[โ€นยซlib/blitz_apex_๐Ÿ‘‰server/stats/aggregate_rank_stats.exยป:ยซ35ยปโ€บ: BlitzApexServer.Stats.AggregateRankStats.update/2]
 897                1 #=> 1");
 898            }
 899
 900            #[test]
 901            // <https://github.com/zed-industries/zed/issues/28194>
 902            fn issue_28194() {
 903                test_path!(
 904                    "โ€นยซtest/c๐Ÿ‘‰ontrollers/template_items_controller_test.rbยป:ยซ20ยปโ€บ:in 'block (2 levels) in <class:TemplateItemsControllerTest>'"
 905                );
 906            }
 907
 908            #[test]
 909            // <https://github.com/zed-industries/zed/issues/50531>
 910            fn issue_50531() {
 911                // Paths preceded by "N:" prefix (e.g. grep output line numbers)
 912                // should still be clickable
 913                test_path!("0: โ€นยซfoo/๐Ÿ‘‰bar.txtยปโ€บ");
 914                test_path!("0: โ€นยซ๐Ÿ‘‰foo/bar.txtยปโ€บ");
 915                test_path!("42: โ€นยซ๐Ÿ‘‰foo/bar.txtยปโ€บ");
 916                test_path!("1: โ€นยซ/๐Ÿ‘‰test/cool.rsยปโ€บ");
 917                test_path!("1: โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ");
 918            }
 919
 920            #[test]
 921            // <https://github.com/zed-industries/zed/issues/46795>
 922            fn issue_46795() {
 923                // Box drawing characters are commonly used as UI elements and
 924                // should not interfere with path detection; they appear rarely
 925                // enough in actual paths that false positives should be minimal
 926
 927                test_path!("โ”€โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ");
 928                test_path!("โ”คโ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ");
 929                test_path!("โ•ฟโ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ");
 930
 931                test_path!("โ””โ”€โ”€โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ");
 932                test_path!("โ”œโ”€[โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ]");
 933                test_path!("โ”€[โ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บ]");
 934                test_path!("โ”ฌโ€นยซ/๐Ÿ‘‰test/cool.rsยป:ยซ4ยป:ยซ2ยปโ€บโ”ฌ");
 935            }
 936
 937            #[test]
 938            #[cfg_attr(
 939                not(target_os = "windows"),
 940                should_panic(
 941                    expected = "Path = ยซ/test/cool.rs:4:NotDescยป, at grid cells (0, 1)..=(7, 2)"
 942                )
 943            )]
 944            #[cfg_attr(
 945                target_os = "windows",
 946                should_panic(
 947                    expected = r#"Path = ยซC:\\test\\cool.rs:4:NotDescยป, at grid cells (0, 1)..=(8, 1)"#
 948                )
 949            )]
 950            // PathWithPosition::parse_str considers "/test/co๐Ÿ‘‰ol.rs:4:NotDesc" invalid input, but
 951            // still succeeds and truncates the part after the position. Ideally this would be
 952            // parsed as the path "/test/co๐Ÿ‘‰ol.rs:4:NotDesc" with no position.
 953            fn path_with_position_parse_str() {
 954                test_path!("`โ€นยซ/test/co๐Ÿ‘‰ol.rs:4:NotDescยปโ€บ`");
 955                test_path!("<โ€นยซ/test/co๐Ÿ‘‰ol.rs:4:NotDescยปโ€บ>");
 956
 957                test_path!("'โ€นยซ(/test/co๐Ÿ‘‰ol.rs:4:2)ยปโ€บ'");
 958                test_path!("'โ€นยซ(/test/co๐Ÿ‘‰ol.rs(4))ยปโ€บ'");
 959                test_path!("'โ€นยซ(/test/co๐Ÿ‘‰ol.rs(4,2))ยปโ€บ'");
 960            }
 961        }
 962
 963        /// Minor issues arguably not important enough to fix/workaround...
 964        mod nits {
 965            #[test]
 966            fn alacritty_bugs_with_two_columns() {
 967                test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rsยป(ยซ4ยป)โ€บ");
 968                test_path!("โ€นยซ/test/cool.rsยป(ยซ๐Ÿ‘‰4ยป)โ€บ");
 969                test_path!("โ€นยซ/test/cool.rsยป(ยซ4ยป,ยซ๐Ÿ‘‰2ยป)โ€บ");
 970
 971                // Python
 972                test_path!("โ€นยซawe๐Ÿ‘‰some.pyยปโ€บ");
 973            }
 974
 975            #[test]
 976            #[cfg_attr(
 977                not(target_os = "windows"),
 978                should_panic(
 979                    expected = "Path = ยซ/test/cool.rsยป, line = 1, at grid cells (0, 0)..=(9, 0)"
 980                )
 981            )]
 982            #[cfg_attr(
 983                target_os = "windows",
 984                should_panic(
 985                    expected = r#"Path = ยซC:\\test\\cool.rsยป, line = 1, at grid cells (0, 0)..=(9, 2)"#
 986                )
 987            )]
 988            fn invalid_row_column_should_be_part_of_path() {
 989                test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rs:1:618033988749ยปโ€บ");
 990                test_path!("โ€นยซ/๐Ÿ‘‰test/cool.rs(1,618033988749)ยปโ€บ");
 991            }
 992
 993            #[test]
 994            #[cfg_attr(
 995                not(target_os = "windows"),
 996                should_panic(expected = "Path = ยซ/te:st/co:ol.r:s:4:2::::::ยป")
 997            )]
 998            #[cfg_attr(
 999                target_os = "windows",
1000                should_panic(expected = r#"Path = ยซC:\\te:st\\co:ol.r:s:4:2::::::ยป"#)
1001            )]
1002            fn many_trailing_colons_should_be_parsed_as_part_of_the_path() {
1003                test_path!("โ€นยซ/te:st/๐Ÿ‘‰co:ol.r:s:4:2::::::ยปโ€บ");
1004                test_path!("/test/cool.rs:::๐Ÿ‘‰:");
1005            }
1006        }
1007
1008        mod windows {
1009            // Lots of fun to be had with long file paths (verbatim) and UNC paths on Windows.
1010            // See <https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation>
1011            // See <https://users.rust-lang.org/t/understanding-windows-paths/58583>
1012            // See <https://github.com/rust-lang/cargo/issues/13919>
1013
1014            #[test]
1015            fn default_prompts() {
1016                // Windows command prompt
1017                test_path!(r#"โ€นยซC:\Users\someone\๐Ÿ‘‰testยปโ€บ>"#);
1018                test_path!(r#"C:\Users\someone\test๐Ÿ‘‰>"#);
1019
1020                // Windows PowerShell
1021                test_path!(r#"PS โ€นยซC:\Users\someone\๐Ÿ‘‰test\cool.rsยปโ€บ>"#);
1022                test_path!(r#"PS C:\Users\someone\test\cool.rs๐Ÿ‘‰>"#);
1023            }
1024
1025            #[test]
1026            fn unc() {
1027                test_path!(r#"โ€นยซ\\server\share\๐Ÿ‘‰test\cool.rsยปโ€บ"#);
1028                test_path!(r#"โ€นยซ\\server\share\test\cool๐Ÿ‘‰.rsยปโ€บ"#);
1029            }
1030
1031            mod issues {
1032                #[test]
1033                fn issue_verbatim() {
1034                    test_path!(r#"โ€นยซ\\?\C:\๐Ÿ‘‰test\cool.rsยปโ€บ"#);
1035                    test_path!(r#"โ€นยซ\\?\C:\test\cool๐Ÿ‘‰.rsยปโ€บ"#);
1036                }
1037
1038                #[test]
1039                fn issue_verbatim_unc() {
1040                    test_path!(r#"โ€นยซ\\?\UNC\server\share\๐Ÿ‘‰test\cool.rsยปโ€บ"#);
1041                    test_path!(r#"โ€นยซ\\?\UNC\server\share\test\cool๐Ÿ‘‰.rsยปโ€บ"#);
1042                }
1043            }
1044        }
1045
1046        mod perf {
1047            use super::super::*;
1048            use crate::TerminalSettings;
1049            use alacritty_terminal::{
1050                event::VoidListener,
1051                grid::Scroll,
1052                index::{Column, Point as AlacPoint},
1053                term::test::mock_term,
1054                term::{Term, search::Match},
1055            };
1056            use settings::{self, Settings, SettingsContent};
1057            use std::{cell::RefCell, rc::Rc};
1058            use util_macros::perf;
1059
1060            fn build_test_term(
1061                line: &str,
1062                repeat: usize,
1063                hover_offset_column: usize,
1064            ) -> (Term<VoidListener>, AlacPoint) {
1065                let content = line.repeat(repeat);
1066                let mut term = mock_term(&content);
1067                term.resize(TermSize {
1068                    columns: 1024,
1069                    screen_lines: 10,
1070                });
1071                term.scroll_display(Scroll::Top);
1072                let point =
1073                    AlacPoint::new(Line(term.topmost_line().0 + 3), Column(hover_offset_column));
1074                (term, point)
1075            }
1076
1077            #[perf]
1078            pub fn cargo_hyperlink_benchmark() {
1079                const LINE: &str = "    Compiling terminal v0.1.0 (/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal)\r\n";
1080                thread_local! {
1081                    static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
1082                        build_test_term(LINE, 500, 50);
1083                }
1084                TEST_TERM_AND_POINT.with(|(term, point)| {
1085                    assert_eq!(
1086                        find_from_grid_point_bench(term, *point)
1087                            .map(|(path, ..)| path)
1088                            .unwrap_or_default(),
1089                        "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal",
1090                        "Hyperlink should have been found"
1091                    );
1092                });
1093            }
1094
1095            #[perf]
1096            pub fn rust_hyperlink_benchmark() {
1097                const LINE: &str = "    --> /Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42\r\n";
1098                thread_local! {
1099                    static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
1100                        build_test_term(LINE, 500, 50);
1101                }
1102                TEST_TERM_AND_POINT.with(|(term, point)| {
1103                    assert_eq!(
1104                        find_from_grid_point_bench(term, *point)
1105                            .map(|(path, ..)| path)
1106                            .unwrap_or_default(),
1107                        "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42",
1108                        "Hyperlink should have been found"
1109                    );
1110                });
1111            }
1112
1113            #[perf]
1114            pub fn ls_hyperlink_benchmark() {
1115                const LINE: &str = "Cargo.toml        experiments        notebooks        rust-toolchain.toml    tooling\r\n";
1116                thread_local! {
1117                    static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
1118                        build_test_term(LINE, 500, 60);
1119                }
1120                TEST_TERM_AND_POINT.with(|(term, point)| {
1121                    assert_eq!(
1122                        find_from_grid_point_bench(term, *point)
1123                            .map(|(path, ..)| path)
1124                            .unwrap_or_default(),
1125                        "rust-toolchain.toml",
1126                        "Hyperlink should have been found"
1127                    );
1128                });
1129            }
1130
1131            #[perf]
1132            // https://github.com/zed-industries/zed/pull/44407
1133            pub fn pr_44407_hyperlink_benchmark() {
1134                const LINE: &str = "-748, 706, 163, 222, -980, 949, 381, -568, 199, 501, 760, -821, 90, -451, 183, 867, -351, -810, -762, -109, 423, 84, 14, -77, -820, -345, 74, -791, 930, -618, -900, 862, -959, 289, -19, 471, -757, 793, 155, -554, 249, 830, 402, 732, -731, -866, -720, -703, -257, -439, 731, 872, -489, 676, -167, 613, -698, 415, -80, -453, -896, 333, -511, 621, -450, 624, -309, -575, 177, 141, 891, -104, -97, -367, -599, -675, 607, -225, -760, 552, -465, 804, 55, 282, 104, -929, -252,\
1135-311, 900, 550, 599, -80, 774, 553, 837, -395, 541, 953, 154, -396, -596, -111, -802, -221, -337, -633, -73, -527, -82, -658, -264, 222, 375, 434, 204, -756, -703, 303, 239, -257, -365, -351, 904, 364, -743, -484, 655, -542, 446, 888, 632, -167, -260, 716, 150, 806, 723, 513, -118, -323, -683, 983, -564, 358, -16, -287, 277, -607, 87, 365, -1, 164, 401, 257, 369, -893, 145, -969, 375, -53, 541, -408, -865, 753, 258, 337, -886, 593, -378, -528, 191, 204, 566, -61, -621, 769, 524, -628, 6,\
1136249, 896, -785, -776, 321, -681, 604, -740, 886, 426, -480, -983, 23, -247, 125, -666, 913, 842, -460, -797, -483, -58, -565, -587, -206, 197, 715, 764, -97, 457, -149, -226, 261, 194, -390, 431, 180, -778, 829, -657, -668, 397, 859, 152, -178, 677, -18, 687, -247, 96, 466, -572, 478, 622, -143, -25, -471, 265, 335, 957, 152, -951, -647, 670, 57, 152, -115, 206, 87, 629, -798, -125, -725, -31, 844, 398, -876, 44, 963, -211, 518, -8, -103, -999, 948, 823, 149, -803, 769, -236, -683, 527,\
1137-108, -36, 18, -437, 687, -305, -526, 972, -965, 276, 420, -259, -379, -142, -747, 600, -578, 197, 673, 890, 324, -931, 755, -765, -422, 785, -369, -110, -505, 532, -208, -438, 713, 110, 853, 996, -360, 823, 289, -699, 629, -661, 560, -329, -323, 439, 571, -537, 644, -84, 25, -536, -161, 112, 169, -922, -537, -734, -423, 37, 451, -149, 408, 18, -672, 206, -784, 444, 593, -241, 502, -259, -798, -352, -658, 712, -675, -734, 627, -620, 64, -554, 999, -537, -160, -641, 464, 894, 29, 322, 566,\
1138-510, -749, 982, 204, 967, -261, -986, -136, 251, -598, 995, -831, 891, 22, 761, -783, -415, 125, 470, -919, -97, -668, 85, 205, -175, -550, 502, 652, -468, 798, 775, -216, 89, -433, -24, -621, 877, -126, 951, 809, 782, 156, -618, -841, -463, 19, -723, -904, 550, 263, 991, -758, -114, 446, -731, -623, -634, 462, 48, 851, 333, -846, 480, 892, -966, -910, -436, 317, -711, -341, -294, 124, 238, -214, -281, 467, -950, -342, 913, -90, -388, -573, 740, -883, -451, 493, -500, 863, 930, 127, 530,\
1139-810, 540, 541, -664, -951, -227, -420, -476, -581, -534, 549, 253, 984, -985, -84, -521, 538, 484, -440, 371, 784, -306, -850, 530, -133, 251, -799, 446, -170, -243, -674, 769, 646, 778, -680, -714, -442, 804, 901, -774, 69, 307, -293, 755, 443, 224, -918, -771, 723, 40, 132, 568, -847, -47, 844, 69, 986, -293, -459, 313, 155, 331, 69, 280, -637, 569, 104, -119, -988, 252, 857, -590, 810, -891, 484, 566, -934, -587, -290, 566, 587, 489, 870, 280, 454, -252, 613, -701, -278, 195, -198,\
1140683, 533, -372, 707, -152, 371, 866, 609, -5, -372, -30, -694, 552, 192, 452, -663, 350, -985, 10, 884, 813, -592, -331, -470, 711, -941, 928, 379, -339, 220, 999, 376, 507, 179, 916, 84, 104, 392, 192, 299, -860, 218, -698, -919, -452, 37, 850, 5, -874, 287, 123, -746, -575, 776, -909, 118, 903, -275, 450, -996, -591, -920, -850, 453, -896, 73, 83, -535, -20, 287, -765, 442, 808, 45, 445, 202, 917, -208, 783, 790, -534, 373, -129, 556, -757, -69, 459, -163, -59, 265, -563, -889, 635,\
1141-583, -261, -790, 799, 826, 953, 85, 619, 334, 842, 672, -869, -4, -833, 315, 942, -524, 579, 926, 628, -404, 128, -629, 161, 568, -117, -526, 223, -876, 906, 176, -549, -317, 381, 375, -801, -416, 647, 335, 253, -386, -375, -254, 635, 352, 317, 398, -422, 111, 201, 220, 554, -972, 853, 378, 956, 942, -857, -289, -333, -180, 488, -814, -42, -595, 721, 39, 644, 721, -242, -44, 643, -457, -419, 560, -863, 974, 458, 222, -882, 526, -243, -318, -343, -707, -401, 117, 677, -489, 546, -903,\
1142-960, -881, -684, 125, -928, -995, -692, -773, 647, -718, -862, -814, 671, 664, -130, -856, -674, 653, 711, 194, -685, -160, 138, -27, -128, -671, -242, 526, 494, -674, 424, -921, -778, 313, -237, 332, 913, 252, 808, -936, 289, 755, 52, -139, 57, -19, -827, -775, -561, -14, 107, -84, 622, -303, -747, 258, -942, 290, 211, -919, -207, 797, 95, 794, -830, -181, -788, 757, 75, -946, -949, -988, 152, 340, 732, 886, -891, -642, -666, 321, -910, 841, 632, 298, 55, -349, 498, 287, -711, 97, 305,\
1143-974, -987, 790, -64, 605, -583, -821, 345, 887, -861, 548, 894, 288, 452, 556, -448, 813, 420, 545, 967, 127, -947, 19, -314, -607, -513, -851, 254, -290, -938, -783, -93, 474, 368, -485, -935, -539, 81, 404, -283, 779, 345, -164, 53, 563, -771, 911, -323, 522, -998, 315, 415, 460, 58, -541, -878, -152, -886, 201, -446, -810, 549, -142, -575, -632, 521, 549, 209, -681, 998, 798, -611, -919, -708, -4, 677, -172, 588, 750, -435, 508, 609, 498, -535, -691, -738, 85, 615, 705, 169, 425,\
1144-669, -491, -783, 73, -847, 228, -981, -812, -229, 950, -904, 175, -438, 632, -556, 910, 173, 576, -751, -53, -169, 635, 607, -944, -13, -84, 105, -644, 984, 935, 259, -445, 620, -405, 832, 167, 114, 209, -181, -944, -496, 693, -473, 137, 38, -873, -334, -353, -57, 397, 944, 698, 811, -401, 712, -667, 905, 276, -653, 368, -543, -349, 414, 287, 894, 935, 461, 55, 741, -623, -660, -773, 617, 834, 278, -121, 52, 495, -855, -440, -210, -99, 279, -661, 540, 934, 540, 784, 895, 268, -503, 513,\
1145-484, -352, 528, 341, -451, 885, -71, 799, -195, -885, -585, -233, 92, 453, 994, 464, 694, 190, -561, -116, 675, -775, -236, 556, -110, -465, 77, -781, 507, -960, -410, 229, -632, 717, 597, 429, 358, -430, -692, -825, 576, 571, 758, -891, 528, -267, 190, -869, 132, -811, 796, 750, -596, -681, 870, 360, 969, 860, -412, -567, 694, -86, -498, 38, -178, -583, -778, 412, 842, -586, 722, -192, 350, 363, 81, -677, -163, 564, 543, 671, 110, 314, 739, -552, -224, -644, 922, 685, 134, 613, 793,\
1146-363, -244, -284, -257, -561, 418, 988, 333, 110, -966, 790, 927, 536, -620, -309, -358, 895, -867, -796, -357, 308, -740, 287, -732, -363, -969, 658, 711, 511, 256, 590, -574, 815, -845, -84, 546, -581, -71, -334, -890, 652, -959, 320, -236, 445, -851, 825, -756, -4, 877, 308, 573, -117, 293, 686, -483, 391, 342, -550, -982, 713, 886, 552, 474, -673, 283, -591, -383, 988, 435, -131, 708, -326, -884, 87, 680, -818, -408, -486, 813, -307, -799, 23, -497, 802, -146, -100, 541, 7, -493, 577,\
114750, -270, 672, 834, 111, -788, 247, 337, 628, -33, -964, -519, 683, 54, -703, 633, -127, -448, 759, -975, 696, 2, -870, -760, 67, 696, 306, 750, 615, 155, -933, -568, 399, 795, 164, -460, 205, 439, -526, -691, 35, -136, -481, -63, 73, -598, 748, 133, 874, -29, 4, -73, 472, 389, 962, 231, -328, 240, 149, 959, 46, -207, 72, -514, -608, 0, -14, 32, 374, -478, -806, 919, -729, -286, 652, 109, 509, -879, -979, -865, 584, -92, -346, -992, 781, 401, 575, 993, -746, -33, 684, -683, 750, -105,\
1148-425, -508, -627, 27, 770, -45, 338, 921, -139, -392, -933, 634, 563, 224, -780, 921, 991, 737, 22, 64, 414, -249, -687, 869, 50, 759, -97, 515, 20, -775, -332, 957, 138, -542, -835, 591, -819, 363, -715, -146, -950, -641, -35, -435, -407, -548, -984, 383, -216, -559, 853, 4, -410, -319, -831, -459, -628, -819, -324, 755, 696, -192, 238, -234, -724, -445, 915, 302, -708, 484, 224, -641, 25, -771, 528, -106, -744, -588, 913, -554, -515, -239, -843, -812, -171, 721, 543, -269, 440, 151,\
1149996, -723, -557, -522, -280, -514, -593, 208, 715, 404, 353, 270, -483, -785, 318, -313, 798, 638, 764, 748, -929, -827, -318, -56, 389, -546, -958, -398, 463, -700, 461, 311, -787, -488, 877, 456, 166, 535, -995, -189, -715, 244, 40, 484, 212, -329, -351, 638, -69, -446, -292, 801, -822, 490, -486, -185, 790, 370, -340, 401, -656, 584, 561, -749, 269, -19, -294, -111, 975, 874, -73, 851, 231, -331, -684, 460, 765, -654, -76, 10, 733, 520, 521, 416, -958, -202, -186, -167, 175, 343, -50,\
1150673, -763, -854, -977, -17, -853, -122, -25, 180, 149, 268, 874, -816, -745, 747, -303, -959, 390, 509, 18, -66, 275, -277, 9, 837, -124, 989, -542, -649, -845, 894, 926, 997, -847, -809, -579, -96, -372, 766, 238, -251, 503, 559, 276, -281, -102, -735, 815, 109, 175, -10, 128, 543, -558, -707, 949, 996, -422, -506, 252, 702, -930, 552, -961, 584, -79, -177, 341, -275, 503, -21, 677, -545, 8, -956, -795, -870, -254, 170, -502, -880, 106, 174, 459, 603, -600, -963, 164, -136, -641, -309,\
1151-380, -707, -727, -10, 727, 952, 997, -731, -133, 269, 287, 855, 716, -650, 479, 299, -839, -308, -782, 769, 545, 663, -536, -115, 904, -986, -258, -562, 582, 664, 408, -525, -889, 471, -370, -534, -220, 310, 766, 931, -193, -897, -192, -74, -365, -256, -359, -328, 658, -691, -431, 406, 699, 425, 713, -584, -45, -588, 289, 658, -290, -880, -987, -444, 371, 904, -155, 81, -278, -708, -189, -78, 655, 342, -998, -647, -734, -218, 726, 619, 663, 744, 518, 60, -409, 561, -727, -961, -306,\
1152-147, -550, 240, -218, -393, 267, 724, 791, -548, 480, 180, -631, 825, -170, 107, 227, -691, 905, -909, 359, 227, 287, 909, 632, -89, -522, 80, -429, 37, 561, -732, -474, 565, -798, -460, 188, 507, -511, -654, 212, -314, -376, -997, -114, -708, 512, -848, 781, 126, -956, -298, 354, -400, -121, 510, 445, 926, 27, -708, 676, 248, 834, 542, 236, -105, -153, 102, 128, 96, -348, -626, 598, 8, 978, -589, -461, -38, 381, -232, -817, 467, 356, -151, -460, 429, -408, 425, 618, -611, -247, 819,\
1153963, -160, 1000, 141, -647, -875, 108, 790, -127, 463, -37, -195, -542, 12, 845, -384, 770, -129, 315, 826, -942, 430, 146, -170, -583, -903, -489, 497, -559, -401, -29, -129, -411, 166, 942, -646, -862, -404, 785, 777, -111, -481, -738, 490, 741, -398, 846, -178, -509, -661, 748, 297, -658, -567, 531, 427, -201, -41, -808, -668, 782, -860, -324, 249, 835, -234, 116, 542, -201, 328, 675, 480, -906, 188, 445, 63, -525, 811, 277, 133, 779, -680, 950, -477, -306, -64, 552, -890, -956, 169,\
1154442, 44, -169, -243, -242, 423, -884, -757, -403, 739, -350, 383, 429, 153, -702, -725, 51, 310, 857, -56, 538, 46, -311, 132, -620, -297, -124, 534, 884, -629, -117, 506, -837, -100, -27, -381, -735, 262, 843, 703, 260, -457, 834, 469, 9, 950, 59, 127, -820, 518, 64, -783, 659, -608, -676, 802, 30, 589, 246, -369, 361, 347, 534, -376, 68, 941, 709, 264, 384, 481, 628, 199, -568, -342, -337, 853, -804, -858, -169, -270, 641, -344, 112, 530, -773, -349, -135, -367, -350, -756, -911, 180,\
1155-660, 116, -478, -265, -581, 510, 520, -986, 935, 219, 522, 744, 47, -145, 917, 638, 301, 296, 858, -721, 511, -816, 328, 473, 441, 697, -260, -673, -379, 893, 458, 154, 86, 905, 590, 231, -717, -179, 79, 272, -439, -192, 178, -200, 51, 717, -256, -358, -626, -518, -314, -825, -325, 588, 675, -892, -798, 448, -518, 603, -23, 668, -655, 845, -314, 783, -347, -496, 921, 893, -163, -748, -906, 11, -143, -64, 300, 336, 882, 646, 533, 676, -98, -148, -607, -952, -481, -959, -874, 764, 537,\
1156736, -347, 646, -843, 966, -916, -718, -391, -648, 740, 755, 919, -608, 388, -655, 68, 201, 675, -855, 7, -503, 881, 760, 669, 831, 721, -564, -445, 217, 331, 970, 521, 486, -254, 25, -259, 336, -831, 252, -995, 908, -412, -240, 123, -478, 366, 264, -504, -843, 632, -288, 896, 301, 423, 185, 318, 380, 457, -450, -162, -313, 673, -963, 570, 433, -548, 107, -39, -142, -98, -884, -3, 599, -486, -926, 923, -82, 686, 290, 99, -382, -789, 16, 495, 570, 284, 474, -504, -201, -178, -1, 592, 52,\
1157827, -540, -151, -991, 130, 353, -420, -467, -661, 417, -690, 942, 936, 814, -566, -251, -298, 341, -139, 786, 129, 525, -861, 680, 955, -245, -50, 331, 412, -38, -66, 611, -558, 392, -629, -471, -68, -535, 744, 495, 87, 558, 695, 260, -308, 215, -464, 239, -50, 193, -540, 184, -8, -194, 148, 898, -557, -21, 884, 644, -785, -689, -281, -737, 267, 50, 206, 292, 265, 380, -511, 310, 53, 375, -497, -40, 312, -606, -395, 142, 422, 662, -584, 72, 144, 40, -679, -593, 581, 689, -829, 442, 822,\
1158977, -832, -134, -248, -207, 248, 29, 259, 189, 592, -834, -866, 102, 0, 340, 25, -354, -239, 420, -730, -992, -925, -314, 420, 914, 607, -296, -415, -30, 813, 866, 153, -90, 150, -81, 636, -392, -222, -835, 482, -631, -962, -413, -727, 280, 686, -382, 157, -404, -511, -432, 455, 58, 108, -408, 290, -829, -252, 113, 550, -935, 925, 422, 38, 789, 361, 487, -460, -769, -963, -285, 206, -799, -488, -233, 416, 143, -456, 753, 520, 599, 621, -168, 178, -841, 51, 952, 374, 166, -300, -576, 844,\
1159-656, 90, 780, 371, 730, -896, -895, -386, -662, 467, -61, 130, -362, -675, -113, 135, -761, -55, 408, 822, 675, -347, 725, 114, 952, -510, -972, 390, -413, -277, -52, 315, -80, 401, -712, 147, -202, 84, 214, -178, 970, -571, -210, 525, -887, -863, 504, 192, 837, -594, 203, -876, -209, 305, -826, 377, 103, -928, -803, -956, 949, -868, -547, 824, -994, 516, 93, -524, -866, -890, -988, -501, 15, -6, 413, -825, 304, -818, -223, 525, 176, 610, 828, 391, 940, 540, -831, 650, 438, 589, 941, 57,\
1160523, 126, 221, 860, -282, -262, -226, 764, 743, -640, 390, 384, -434, 608, -983, 566, -446, 618, 456, -176, -278, 215, 871, -180, 444, -931, -200, -781, 404, 881, 780, -782, 517, -739, -548, -811, 201, -95, -249, -228, 491, -299, 700, 964, -550, 108, 334, -653, 245, -293, -552, 350, -685, -415, -818, 216, -194, -255, 295, 249, 408, 351, 287, 379, 682, 231, -693, 902, -902, 574, 937, -708, -402, -460, 827, -268, 791, 343, -780, -150, -738, 920, -430, -88, -361, -588, -727, -47, -297, 662,\
1161-840, -637, -635, 916, -857, 938, 132, -553, 391, -522, 640, 626, 690, 833, 867, -555, 577, 226, 686, -44, 0, -965, 651, -1, 909, 595, -646, 740, -821, -648, -962, 927, -193, 159, 490, 594, -189, 707, -884, 759, -278, -160, -566, -340, 19, 862, -440, 445, -598, 341, 664, -311, 309, -159, 19, -672, 705, -646, 976, 247, 686, -830, -27, -667, 81, 399, -423, -567, 945, 38, 51, 740, 621, 204, -199, -908, -593, 424, 250, -561, 695, 9, 520, 878, 120, -109, 42, -375, -635, -711, -687, 383, -278,\
116236, 970, 925, 864, 836, 309, 117, 89, 654, -387, 346, -53, 617, -164, -624, 184, -45, 852, 498, -513, 794, -682, -576, 13, -147, 285, -776, -886, -96, 483, 994, -188, 346, -629, -848, 738, 51, 128, -898, -753, -906, 270, -203, -577, 48, -243, -210, 666, 353, 636, -954, 862, 560, -944, -877, -137, 440, -945, -316, 274, -211, -435, 615, -635, -468, 744, 948, -589, 525, 757, -191, -431, 42, 451, -160, -827, -991, 324, 697, 342, -610, 894, -787, -384, 872, 734, 878, 70, -260, 57, 397, -518,\
1163629, -510, -94, 207, 214, -625, 106, -882, -575, 908, -650, 723, -154, 45, 108, -69, -565, 927, -68, -351, 707, -282, 429, -889, -596, 848, 578, -492, 41, -822, -992, 168, -286, -780, 970, 597, -293, -12, 367, 708, -415, 194, -86, -390, 224, 69, -368, -674, 1000, -672, 356, -202, -169, 826, 476, -285, 29, -448, 545, 186, 319, 67, 705, 412, 225, -212, -351, -391, -783, -9, 875, -59, -159, -123, -151, -296, 871, -638, 359, 909, -945, 345, -16, -562, -363, -183, -625, -115, -571, -329, 514,\
116499, 263, 463, -39, 597, -652, -349, 246, 77, -127, -563, -879, -30, 756, 777, -865, 675, -813, -501, 871, -406, -627, 834, -609, -205, -812, 643, -204, 291, -251, -184, -584, -541, 410, -573, -600, 908, -871, -687, 296, -713, -139, -778, -790, 347, -52, -400, 407, -653, 670, 39, -856, 904, 433, 392, 590, -271, -144, -863, 443, 353, 468, -544, 486, -930, 458, -596, -890, 163, 822, 768, 980, -783, -792, 126, 386, 367, -264, 603, -61, 728, 160, -4, -837, 832, 591, 436, 518, 796, -622, -867,\
1165-669, -947, 253, 100, -792, 841, 413, 833, -249, -550, 282, -825, 936, -348, 898, -451, -283, 818, -237, 630, 216, -499, -637, -511, 767, -396, 221, 958, -586, -920, 401, -313, -580, -145, -270, 118, 497, 426, -975, 480, -445, -150, -721, -929, 439, -893, 902, 960, -525, -793, 924, 563, 683, -727, -86, 309, 432, -762, -345, 371, -617, 149, -215, -228, 505, 593, -20, -292, 704, -999, 149, -104, 819, -414, -443, 517, -599, -5, 145, -24, -993, -283, 904, 174, -112, -276, -860, 44, -257,\
1166-931, -821, -667, 540, 421, 485, 531, 407, 833, 431, -415, 878, 503, -901, 639, -608, 896, 860, 927, 424, 113, -808, -323, 729, 382, -922, 548, -791, -379, 207, 203, 559, 537, 137, 999, -913, -240, 942, 249, 616, 775, -4, 915, 855, -987, -234, -384, 948, -310, -542, 125, -289, -599, 967, -492, -349, -552, 562, -926, 632, -164, 217, -165, -496, 847, 684, -884, 457, -748, -745, -38, 93, 961, 934, 588, 366, -130, 851, -803, -811, -211, 428, 183, -469, 888, 596, -475, -899, -681, 508, 184,\
1167921, 863, -610, -416, -119, -966, -686, 210, 733, 715, -889, -925, -434, -566, -455, 596, -514, 983, 755, -194, -802, -313, 91, -541, 808, -834, 243, -377, 256, 966, -402, -773, -308, -605, 266, 866, 118, -425, -531, 498, 666, 813, -267, 830, 69, -869, -496, 735, 28, 488, -645, -493, -689, 170, -940, 532, 844, -658, -617, 408, -200, 764, -665, 568, 342, 621, 908, 471, 280, 859, 709, 898, 81, -547, 406, 514, -595, 43, -824, -696, -746, -429, -59, -263, -813, 233, 279, -125, 687, -418,\
1168-530, 409, 614, 803, -407, 78, -676, -39, -887, -141, -292, 270, -343, 400, 907, 588, 668, 899, 973, 103, -101, -11, 397, -16, 165, 705, -410, -585, 316, 391, -346, -336, 957, -118, -538, -441, -845, 121, 591, -359, -188, -362, -208, 27, -925, -157, -495, -177, -580, 9, 531, -752, 94, 107, 820, 769, -500, 852, 617, 145, 355, 34, -463, -265, -709, -111, -855, -405, 560, 470, 3, -177, -164, -249, 450, 662, 841, -689, -509, 987, -33, 769, 234, -2, 203, 780, 744, -895, 497, -432, -406, -264,\
1169-71, 124, 778, -897, 495, 127, -76, 52, -768, 205, 464, -992, 801, -83, -806, 545, -316, 146, 772, 786, 289, -936, 145, -30, -722, -455, 270, 444, 427, -482, 383, -861, 36, 630, -404, 83, 864, 743, -351, -846, 315, -837, 357, -195, 450, -715, 227, -942, 740, -519, 476, 716, 713, 169, 492, -112, -49, -931, 866, 95, -725, 198, -50, -17, -660, 356, -142, -781, 53, 431, 720, 143, -416, 446, -497, 490, -96, 157, 239, 487, -337, -224, -445, 813, 92, -22, 603, 424, 952, -632, -367, 898, -927,\
1170884, -277, -187, -777, 537, -575, -313, 347, -33, 800, 672, -919, -541, 5, -270, -94, -265, -793, -183, -761, -516, -608, -218, 57, -889, -912, 508, 93, -90, 34, 530, 201, 999, -37, -186, -62, -980, 239, 902, 983, -287, -634, 524, -772, 470, -961, 32, 162, 315, -411, 400, -235, -283, -787, -703, 869, 792, 543, -274, 239, 733, -439, 306, 349, 579, -200, -201, -824, 384, -246, 133, -508, 770, -102, 957, -825, 740, 748, -376, 183, -426, 46, 668, -886, -43, -174, 672, -419, 390, 927, 1000,\
1171318, 886, 47, 908, -540, -825, -5, 314, -999, 354, -603, 966, -633, -689, 985, 534, -290, 167, -652, -797, -612, -79, 488, 622, -464, -950, 595, 897, 704, -238, -395, 125, 831, -180, 226, -379, 310, 564, 56, -978, 895, -61, 686, -251, 434, -417, 161, -512, 752, 528, -589, -425, 66, -925, -157, 1000, 96, 256, -239, -784, -882, -464, -909, 663, -177, -678, -441, 669, -564, -201, -121, -743, 187, -107, -768, -682, 355, 161, 411, 984, -954, 166, -842, -755, 267, -709, 372, -699, -272, -850,\
1172403, -839, 949, 622, -62, 51, 917, 70, 528, -558, -632, 832, 276, 61, -445, -195, 960, 846, -474, 764, 879, -411, 948, -62, -592, -123, -96, -551, -555, -724, 849, 250, -808, -732, 797, -839, -554, 306, -919, 888, 484, -728, 152, -122, -287, 16, -345, -396, -268, -963, -500, 433, 343, 418, -480, 828, 594, 821, -9, 933, -230, 707, -847, -610, -748, -234, 688, 935, 713, 865, -743, 293, -143, -20, 928, -906, -762, 528, 722, 412, -70, 622, -245, 539, -686, 730, -866, -705, 28, -916, -623,\
1173-768, -614, -915, -123, -183, 680, -223, 515, -37, -235, -5, 260, 347, -239, -322, -861, -848, -936, 945, 721, -580, -639, 780, -153, -26, 685, 177, 587, 307, -915, 435, 658, 539, -229, -719, -171, -858, 162, 734, -539, -437, 246, 639, 765, -477, -342, -209, -284, -779, -414, -452, 914, 338, -83, 759, 567, 266, -485, 14, 225, 347, -432, -242, 997, -365, -764, 119, -641, -416, -388, -436, -388, -54, -649, -571, -920, -477, 714, -363, 836, 369, 702, 869, 503, -287, -679, 46, -666, -202,\
1174-602, 71, -259, 967, 601, -571, -830, -993, -271, 281, -494, 482, -180, 572, 587, -651, -566, -448, -228, 511, -924, 832, -52, -712, 402, -644, -533, -865, 269, 965, 56, 675, 179, -338, -272, 614, 602, -283, 303, -70, 909, -942, 117, 839, 468, 813, -765, 884, -697, -813, 352, 374, -705, -295, 633, 211, -754, 597, -941, -142, -393, -469, -653, 688, 996, 911, 214, 431, 453, -141, 874, -81, -258, -735, -3, -110, -338, -929, -182, -306, -104, -840, -588, -759, -157, -801, 848, -698, 627, 914,\
1175-33, -353, 425, 150, -798, 553, 934, -778, -196, -132, 808, 745, -894, 144, 213, 662, 273, -79, 454, -60, -467, 48, -15, -807, 69, -930, 749, 559, -867, -103, 258, -677, 750, -303, 846, -227, -936, 744, -770, 770, -434, 594, -477, 589, -612, 535, 357, -623, 683, 369, 905, 980, -410, -663, 762, -888, -563, -845, 843, 353, -491, 996, -255, -336, -132, 695, -823, 289, -143, 365, 916, 877, 245, -530, -848, -804, -118, -108, 847, 620, -355, 499, 881, 92, -640, 542, 38, 626, -260, -34, -378,\
1176598, 890, 305, -118, 711, -385, 600, -570, 27, -129, -893, 354, 459, 374, 816, 470, 356, 661, 877, 735, -286, -780, 620, 943, -169, -888, 978, 441, -667, -399, 662, 249, 137, 598, -863, -453, 722, -815, -251, -995, -294, -707, 901, 763, 977, 137, 431, -994, 905, 593, 694, 444, -626, -816, 252, 282, 616, 841, 360, -932, 817, -908, 50, 394, -120, -786, -338, 499, -982, -95, -454, 838, -312, 320, -127, -653, 53, 16, 988, -968, -151, -369, -836, 293, -271, 483, 18, 724, -204, -965, 245, 310,\
1177987, 552, -835, -912, -861, 254, 560, 124, 145, 798, 178, 476, 138, -311, 151, -907, -886, -592, 728, -43, -489, 873, -422, -439, -489, 375, -703, -459, 338, 418, -25, 332, -454, 730, -604, -800, 37, -172, -197, -568, -563, -332, 228, -182, 994, -123, 444, -567, 98, 78, 0, -504, -150, 88, -936, 199, -651, -776, 192, 46, 526, -727, -991, 534, -659, -738, 256, -894, 965, -76, 816, 435, -418, 800, 838, 67, -733, 570, 112, -514, -416\r\
1178";
1179                thread_local! {
1180                    static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
1181                        build_test_term(&LINE, 5, 50);
1182                }
1183                TEST_TERM_AND_POINT.with(|(term, point)| {
1184                    assert_eq!(
1185                        find_from_grid_point_bench(term, *point)
1186                            .map(|(path, ..)| path)
1187                            .unwrap_or_default(),
1188                        "392",
1189                        "Hyperlink should have been found"
1190                    );
1191                });
1192            }
1193
1194            #[perf]
1195            // https://github.com/zed-industries/zed/issues/44510
1196            pub fn issue_44510_hyperlink_benchmark() {
1197                const LINE: &str = "..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1198..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1199..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1200..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1201..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1202..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1203..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1204..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1205..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1206..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1207..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1208..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1209..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1210..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
1211...............................................E.\r\
1212";
1213                thread_local! {
1214                    static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
1215                        build_test_term(&LINE, 5, 50);
1216                }
1217                TEST_TERM_AND_POINT.with(|(term, point)| {
1218                    assert_eq!(
1219                        find_from_grid_point_bench(term, *point)
1220                            .map(|(path, ..)| path)
1221                            .unwrap_or_default(),
1222                        LINE.trim_end_matches(['.', '\r', '\n']),
1223                        "Hyperlink should have been found"
1224                    );
1225                });
1226            }
1227
1228            pub fn find_from_grid_point_bench(
1229                term: &Term<VoidListener>,
1230                point: AlacPoint,
1231            ) -> Option<(String, bool, Match)> {
1232                const PATH_HYPERLINK_TIMEOUT_MS: u64 = 1000;
1233
1234                thread_local! {
1235                    static TEST_REGEX_SEARCHES: RefCell<RegexSearches> =
1236                        RefCell::new({
1237                            let default_settings_content: Rc<SettingsContent> =
1238                                settings::parse_json_with_comments(&settings::default_settings())
1239                                    .unwrap();
1240                            let default_terminal_settings =
1241                                TerminalSettings::from_settings(&default_settings_content);
1242
1243                            RegexSearches::new(
1244                                &default_terminal_settings.path_hyperlink_regexes,
1245                                PATH_HYPERLINK_TIMEOUT_MS
1246                            )
1247                        });
1248                }
1249
1250                TEST_REGEX_SEARCHES.with(|regex_searches| {
1251                    find_from_grid_point(
1252                        &term,
1253                        point,
1254                        &mut regex_searches.borrow_mut(),
1255                        PathStyle::local(),
1256                    )
1257                })
1258            }
1259        }
1260    }
1261
1262    mod file_iri {
1263        // File IRIs have a ton of use cases. Absolute file URIs are supported on all platforms,
1264        // including Windows drive letters (e.g., file:///C:/path) and percent-encoded characters.
1265        // Some cases like relative file IRIs are not supported.
1266        // See https://en.wikipedia.org/wiki/File_URI_scheme
1267
1268        /// [**`cโ‚€, cโ‚, โ€ฆ, cโ‚™;`**]โ‚’โ‚šโ‚œ := use specified terminal widths of `cโ‚€, cโ‚, โ€ฆ, cโ‚™` **columns**
1269        /// (defaults to `3, longest_line_cells / 2, longest_line_cells + 1;`)
1270        ///
1271        macro_rules! test_file_iri {
1272            ($file_iri:literal) => { { test_hyperlink!(concat!("โ€นยซ๐Ÿ‘‰", $file_iri, "ยปโ€บ"); FileIri) } };
1273        }
1274
1275        #[cfg(not(target_os = "windows"))]
1276        #[test]
1277        fn absolute_file_iri() {
1278            test_file_iri!("file:///test/cool/index.rs");
1279            test_file_iri!("file:///test/cool/");
1280        }
1281
1282        mod issues {
1283            #[cfg(not(target_os = "windows"))]
1284            #[test]
1285            fn issue_file_iri_with_percent_encoded_characters() {
1286                // Non-space characters
1287                // file:///test/แฟฌฯŒฮดฮฟฯ‚/
1288                test_file_iri!("file:///test/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82/"); // URI
1289
1290                // Spaces
1291                test_file_iri!("file:///te%20st/co%20ol/index.rs");
1292                test_file_iri!("file:///te%20st/co%20ol/");
1293            }
1294        }
1295
1296        #[cfg(target_os = "windows")]
1297        mod windows {
1298            mod issues {
1299                // The test uses Url::to_file_path(), but it seems that the Url crate doesn't
1300                // support relative file IRIs.
1301                #[test]
1302                #[should_panic(
1303                    expected = r#"Failed to interpret file IRI `file:/test/cool/index.rs` as a path"#
1304                )]
1305                fn issue_relative_file_iri() {
1306                    test_file_iri!("file:/test/cool/index.rs");
1307                    test_file_iri!("file:/test/cool/");
1308                }
1309
1310                // See https://en.wikipedia.org/wiki/File_URI_scheme
1311                // https://github.com/zed-industries/zed/issues/39189
1312                #[test]
1313                fn issue_39189() {
1314                    test_file_iri!("file:///C:/test/cool/index.rs");
1315                    test_file_iri!("file:///C:/test/cool/");
1316                }
1317
1318                #[test]
1319                fn issue_file_iri_with_percent_encoded_characters() {
1320                    // Non-space characters
1321                    // file:///test/แฟฌฯŒฮดฮฟฯ‚/
1322                    test_file_iri!("file:///C:/test/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82/"); // URI
1323
1324                    // Spaces
1325                    test_file_iri!("file:///C:/te%20st/co%20ol/index.rs");
1326                    test_file_iri!("file:///C:/te%20st/co%20ol/");
1327                }
1328            }
1329        }
1330    }
1331
1332    mod iri {
1333        /// [**`cโ‚€, cโ‚, โ€ฆ, cโ‚™;`**]โ‚’โ‚šโ‚œ := use specified terminal widths of `cโ‚€, cโ‚, โ€ฆ, cโ‚™` **columns**
1334        /// (defaults to `3, longest_line_cells / 2, longest_line_cells + 1;`)
1335        ///
1336        macro_rules! test_iri {
1337            ($iri:literal) => { { test_hyperlink!(concat!("โ€นยซ๐Ÿ‘‰", $iri, "ยปโ€บ"); Iri) } };
1338        }
1339
1340        #[test]
1341        fn simple() {
1342            // In the order they appear in URL_REGEX, except 'file://' which is treated as a path
1343            test_iri!("ipfs://test/cool.ipfs");
1344            test_iri!("ipns://test/cool.ipns");
1345            test_iri!("magnet://test/cool.git");
1346            test_iri!("mailto:someone@somewhere.here");
1347            test_iri!("gemini://somewhere.here");
1348            test_iri!("gopher://somewhere.here");
1349            test_iri!("http://test/cool/index.html");
1350            test_iri!("http://10.10.10.10:1111/cool.html");
1351            test_iri!("http://test/cool/index.html?amazing=1");
1352            test_iri!("http://test/cool/index.html#right%20here");
1353            test_iri!("http://test/cool/index.html?amazing=1#right%20here");
1354            test_iri!("https://test/cool/index.html");
1355            test_iri!("https://10.10.10.10:1111/cool.html");
1356            test_iri!("https://test/cool/index.html?amazing=1");
1357            test_iri!("https://test/cool/index.html#right%20here");
1358            test_iri!("https://test/cool/index.html?amazing=1#right%20here");
1359            test_iri!("news://test/cool.news");
1360            test_iri!("git://test/cool.git");
1361            test_iri!("ssh://user@somewhere.over.here:12345/test/cool.git");
1362            test_iri!("ftp://test/cool.ftp");
1363        }
1364
1365        #[test]
1366        fn wide_chars() {
1367            // In the order they appear in URL_REGEX, except 'file://' which is treated as a path
1368            test_iri!("ipfs://ไพ‹๐Ÿƒ๐Ÿฆ€/cool.ipfs");
1369            test_iri!("ipns://ไพ‹๐Ÿƒ๐Ÿฆ€/cool.ipns");
1370            test_iri!("magnet://ไพ‹๐Ÿƒ๐Ÿฆ€/cool.git");
1371            test_iri!("mailto:someone@somewhere.here");
1372            test_iri!("gemini://somewhere.here");
1373            test_iri!("gopher://somewhere.here");
1374            test_iri!("http://ไพ‹๐Ÿƒ๐Ÿฆ€/cool/index.html");
1375            test_iri!("http://10.10.10.10:1111/cool.html");
1376            test_iri!("http://ไพ‹๐Ÿƒ๐Ÿฆ€/cool/index.html?amazing=1");
1377            test_iri!("http://ไพ‹๐Ÿƒ๐Ÿฆ€/cool/index.html#right%20here");
1378            test_iri!("http://ไพ‹๐Ÿƒ๐Ÿฆ€/cool/index.html?amazing=1#right%20here");
1379            test_iri!("https://ไพ‹๐Ÿƒ๐Ÿฆ€/cool/index.html");
1380            test_iri!("https://10.10.10.10:1111/cool.html");
1381            test_iri!("https://ไพ‹๐Ÿƒ๐Ÿฆ€/cool/index.html?amazing=1");
1382            test_iri!("https://ไพ‹๐Ÿƒ๐Ÿฆ€/cool/index.html#right%20here");
1383            test_iri!("https://ไพ‹๐Ÿƒ๐Ÿฆ€/cool/index.html?amazing=1#right%20here");
1384            test_iri!("news://ไพ‹๐Ÿƒ๐Ÿฆ€/cool.news");
1385            test_iri!("git://ไพ‹/cool.git");
1386            test_iri!("ssh://user@somewhere.over.here:12345/ไพ‹๐Ÿƒ๐Ÿฆ€/cool.git");
1387            test_iri!("ftp://ไพ‹๐Ÿƒ๐Ÿฆ€/cool.ftp");
1388        }
1389
1390        // There are likely more tests needed for IRI vs URI
1391        #[test]
1392        fn iris() {
1393            // These refer to the same location, see example here:
1394            // <https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier#Compatibility>
1395            test_iri!("https://en.wiktionary.org/wiki/แฟฌฯŒฮดฮฟฯ‚"); // IRI
1396            test_iri!("https://en.wiktionary.org/wiki/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82"); // URI
1397        }
1398
1399        #[test]
1400        #[should_panic(expected = "Expected a path, but was a iri")]
1401        fn file_is_a_path() {
1402            test_iri!("file://test/cool/index.rs");
1403        }
1404    }
1405
1406    #[derive(Debug, PartialEq)]
1407    enum HyperlinkKind {
1408        FileIri,
1409        Iri,
1410        Path,
1411    }
1412
1413    struct ExpectedHyperlink {
1414        hovered_grid_point: AlacPoint,
1415        hovered_char: char,
1416        hyperlink_kind: HyperlinkKind,
1417        iri_or_path: String,
1418        row: Option<u32>,
1419        column: Option<u32>,
1420        hyperlink_match: RangeInclusive<AlacPoint>,
1421    }
1422
1423    /// Converts to Windows style paths on Windows, like path!(), but at runtime for improved test
1424    /// readability.
1425    fn build_term_from_test_lines<'a>(
1426        hyperlink_kind: HyperlinkKind,
1427        term_size: TermSize,
1428        test_lines: impl Iterator<Item = &'a str>,
1429    ) -> (Term<VoidListener>, ExpectedHyperlink) {
1430        #[derive(Default, Eq, PartialEq)]
1431        enum HoveredState {
1432            #[default]
1433            HoveredScan,
1434            HoveredNextChar,
1435            Done,
1436        }
1437
1438        #[derive(Default, Eq, PartialEq)]
1439        enum MatchState {
1440            #[default]
1441            MatchScan,
1442            MatchNextChar,
1443            Match(AlacPoint),
1444            Done,
1445        }
1446
1447        #[derive(Default, Eq, PartialEq)]
1448        enum CapturesState {
1449            #[default]
1450            PathScan,
1451            PathNextChar,
1452            Path(AlacPoint),
1453            RowScan,
1454            Row(String),
1455            ColumnScan,
1456            Column(String),
1457            Done,
1458        }
1459
1460        fn prev_input_point_from_term(term: &Term<VoidListener>) -> AlacPoint {
1461            let grid = term.grid();
1462            let cursor = &grid.cursor;
1463            let mut point = cursor.point;
1464
1465            if !cursor.input_needs_wrap {
1466                point = point.sub(term, Boundary::Grid, 1);
1467            }
1468
1469            if grid.index(point).flags.contains(Flags::WIDE_CHAR_SPACER) {
1470                point.column -= 1;
1471            }
1472
1473            point
1474        }
1475
1476        fn end_point_from_prev_input_point(
1477            term: &Term<VoidListener>,
1478            prev_input_point: AlacPoint,
1479        ) -> AlacPoint {
1480            if term
1481                .grid()
1482                .index(prev_input_point)
1483                .flags
1484                .contains(Flags::WIDE_CHAR)
1485            {
1486                prev_input_point.add(term, Boundary::Grid, 1)
1487            } else {
1488                prev_input_point
1489            }
1490        }
1491
1492        fn process_input(term: &mut Term<VoidListener>, c: char) {
1493            match c {
1494                '\t' => term.put_tab(1),
1495                c @ _ => term.input(c),
1496            }
1497        }
1498
1499        let mut hovered_grid_point: Option<AlacPoint> = None;
1500        let mut hyperlink_match = AlacPoint::default()..=AlacPoint::default();
1501        let mut iri_or_path = String::default();
1502        let mut row = None;
1503        let mut column = None;
1504        let mut prev_input_point = AlacPoint::default();
1505        let mut hovered_state = HoveredState::default();
1506        let mut match_state = MatchState::default();
1507        let mut captures_state = CapturesState::default();
1508        let mut term = Term::new(Config::default(), &term_size, VoidListener);
1509
1510        for text in test_lines {
1511            let chars: Box<dyn Iterator<Item = char>> =
1512                if cfg!(windows) && hyperlink_kind == HyperlinkKind::Path {
1513                    Box::new(text.chars().map(|c| if c == '/' { '\\' } else { c })) as _
1514                } else {
1515                    Box::new(text.chars()) as _
1516                };
1517            let mut chars = chars.peekable();
1518            while let Some(c) = chars.next() {
1519                match c {
1520                    '๐Ÿ‘‰' => {
1521                        hovered_state = HoveredState::HoveredNextChar;
1522                    }
1523                    '๐Ÿ‘ˆ' => {
1524                        hovered_grid_point = Some(prev_input_point.add(&term, Boundary::Grid, 1));
1525                    }
1526                    'ยซ' | 'ยป' => {
1527                        captures_state = match captures_state {
1528                            CapturesState::PathScan => CapturesState::PathNextChar,
1529                            CapturesState::PathNextChar => {
1530                                panic!("Should have been handled by char input")
1531                            }
1532                            CapturesState::Path(start_point) => {
1533                                iri_or_path = term.bounds_to_string(
1534                                    start_point,
1535                                    end_point_from_prev_input_point(&term, prev_input_point),
1536                                );
1537                                CapturesState::RowScan
1538                            }
1539                            CapturesState::RowScan => CapturesState::Row(String::new()),
1540                            CapturesState::Row(number) => {
1541                                row = Some(number.parse::<u32>().unwrap());
1542                                CapturesState::ColumnScan
1543                            }
1544                            CapturesState::ColumnScan => CapturesState::Column(String::new()),
1545                            CapturesState::Column(number) => {
1546                                column = Some(number.parse::<u32>().unwrap());
1547                                CapturesState::Done
1548                            }
1549                            CapturesState::Done => {
1550                                panic!("Extra 'ยซ', 'ยป'")
1551                            }
1552                        }
1553                    }
1554                    'โ€น' | 'โ€บ' => {
1555                        match_state = match match_state {
1556                            MatchState::MatchScan => MatchState::MatchNextChar,
1557                            MatchState::MatchNextChar => {
1558                                panic!("Should have been handled by char input")
1559                            }
1560                            MatchState::Match(start_point) => {
1561                                hyperlink_match = start_point
1562                                    ..=end_point_from_prev_input_point(&term, prev_input_point);
1563                                MatchState::Done
1564                            }
1565                            MatchState::Done => {
1566                                panic!("Extra 'โ€น', 'โ€บ'")
1567                            }
1568                        }
1569                    }
1570                    _ => {
1571                        if let CapturesState::Row(number) | CapturesState::Column(number) =
1572                            &mut captures_state
1573                        {
1574                            number.push(c)
1575                        }
1576
1577                        let is_windows_abs_path_start = captures_state
1578                            == CapturesState::PathNextChar
1579                            && cfg!(windows)
1580                            && hyperlink_kind == HyperlinkKind::Path
1581                            && c == '\\'
1582                            && chars.peek().is_some_and(|c| *c != '\\');
1583
1584                        if is_windows_abs_path_start {
1585                            // Convert Unix abs path start into Windows abs path start so that the
1586                            // same test can be used for both OSes.
1587                            term.input('C');
1588                            prev_input_point = prev_input_point_from_term(&term);
1589                            term.input(':');
1590                            process_input(&mut term, c);
1591                        } else {
1592                            process_input(&mut term, c);
1593                            prev_input_point = prev_input_point_from_term(&term);
1594                        }
1595
1596                        if hovered_state == HoveredState::HoveredNextChar {
1597                            hovered_grid_point = Some(prev_input_point);
1598                            hovered_state = HoveredState::Done;
1599                        }
1600                        if captures_state == CapturesState::PathNextChar {
1601                            captures_state = CapturesState::Path(prev_input_point);
1602                        }
1603                        if match_state == MatchState::MatchNextChar {
1604                            match_state = MatchState::Match(prev_input_point);
1605                        }
1606                    }
1607                }
1608            }
1609            term.move_down_and_cr(1);
1610        }
1611
1612        if hyperlink_kind == HyperlinkKind::FileIri {
1613            let Ok(url) = Url::parse(&iri_or_path) else {
1614                panic!("Failed to parse file IRI `{iri_or_path}`");
1615            };
1616            let Ok(path) = url.to_file_path() else {
1617                panic!("Failed to interpret file IRI `{iri_or_path}` as a path");
1618            };
1619            iri_or_path = path.to_string_lossy().into_owned();
1620        }
1621
1622        let hovered_grid_point = hovered_grid_point.expect("Missing hovered point (๐Ÿ‘‰ or ๐Ÿ‘ˆ)");
1623        let hovered_char = term.grid().index(hovered_grid_point).c;
1624        (
1625            term,
1626            ExpectedHyperlink {
1627                hovered_grid_point,
1628                hovered_char,
1629                hyperlink_kind,
1630                iri_or_path,
1631                row,
1632                column,
1633                hyperlink_match,
1634            },
1635        )
1636    }
1637
1638    fn line_cells_count(line: &str) -> usize {
1639        // This avoids taking a dependency on the unicode-width crate
1640        fn width(c: char) -> usize {
1641            match c {
1642                // Fullwidth unicode characters used in tests
1643                'ไพ‹' | '๐Ÿƒ' | '๐Ÿฆ€' | '๐Ÿ”ฅ' => 2,
1644                '\t' => 8, // it's really 0-8, use the max always
1645                _ => 1,
1646            }
1647        }
1648        const CONTROL_CHARS: &str = "โ€นยซ๐Ÿ‘‰๐Ÿ‘ˆยปโ€บ";
1649        line.chars()
1650            .filter(|c| !CONTROL_CHARS.contains(*c))
1651            .map(width)
1652            .sum::<usize>()
1653    }
1654
1655    struct CheckHyperlinkMatch<'a> {
1656        term: &'a Term<VoidListener>,
1657        expected_hyperlink: &'a ExpectedHyperlink,
1658        source_location: &'a str,
1659    }
1660
1661    impl<'a> CheckHyperlinkMatch<'a> {
1662        fn new(
1663            term: &'a Term<VoidListener>,
1664            expected_hyperlink: &'a ExpectedHyperlink,
1665            source_location: &'a str,
1666        ) -> Self {
1667            Self {
1668                term,
1669                expected_hyperlink,
1670                source_location,
1671            }
1672        }
1673
1674        fn check_path_with_position_and_match(
1675            &self,
1676            path_with_position: PathWithPosition,
1677            hyperlink_match: &Match,
1678        ) {
1679            let format_path_with_position_and_match =
1680                |path_with_position: &PathWithPosition, hyperlink_match: &Match| {
1681                    let mut result =
1682                        format!("Path = ยซ{}ยป", &path_with_position.path.to_string_lossy());
1683                    if let Some(row) = path_with_position.row {
1684                        result += &format!(", line = {row}");
1685                        if let Some(column) = path_with_position.column {
1686                            result += &format!(", column = {column}");
1687                        }
1688                    }
1689
1690                    result += &format!(
1691                        ", at grid cells {}",
1692                        Self::format_hyperlink_match(hyperlink_match)
1693                    );
1694                    result
1695                };
1696
1697            assert_ne!(
1698                self.expected_hyperlink.hyperlink_kind,
1699                HyperlinkKind::Iri,
1700                "\n    at {}\nExpected a path, but was a iri:\n{}",
1701                self.source_location,
1702                self.format_renderable_content()
1703            );
1704
1705            assert_eq!(
1706                format_path_with_position_and_match(
1707                    &PathWithPosition {
1708                        path: PathBuf::from(self.expected_hyperlink.iri_or_path.clone()),
1709                        row: self.expected_hyperlink.row,
1710                        column: self.expected_hyperlink.column
1711                    },
1712                    &self.expected_hyperlink.hyperlink_match
1713                ),
1714                format_path_with_position_and_match(&path_with_position, hyperlink_match),
1715                "\n    at {}:\n{}",
1716                self.source_location,
1717                self.format_renderable_content()
1718            );
1719        }
1720
1721        fn check_iri_and_match(&self, iri: String, hyperlink_match: &Match) {
1722            let format_iri_and_match = |iri: &String, hyperlink_match: &Match| {
1723                format!(
1724                    "Url = ยซ{iri}ยป, at grid cells {}",
1725                    Self::format_hyperlink_match(hyperlink_match)
1726                )
1727            };
1728
1729            assert_eq!(
1730                self.expected_hyperlink.hyperlink_kind,
1731                HyperlinkKind::Iri,
1732                "\n    at {}\nExpected a iri, but was a path:\n{}",
1733                self.source_location,
1734                self.format_renderable_content()
1735            );
1736
1737            assert_eq!(
1738                format_iri_and_match(
1739                    &self.expected_hyperlink.iri_or_path,
1740                    &self.expected_hyperlink.hyperlink_match
1741                ),
1742                format_iri_and_match(&iri, hyperlink_match),
1743                "\n    at {}:\n{}",
1744                self.source_location,
1745                self.format_renderable_content()
1746            );
1747        }
1748
1749        fn format_hyperlink_match(hyperlink_match: &Match) -> String {
1750            format!(
1751                "({}, {})..=({}, {})",
1752                hyperlink_match.start().line.0,
1753                hyperlink_match.start().column.0,
1754                hyperlink_match.end().line.0,
1755                hyperlink_match.end().column.0
1756            )
1757        }
1758
1759        fn format_renderable_content(&self) -> String {
1760            let mut result = format!("\nHovered on '{}'\n", self.expected_hyperlink.hovered_char);
1761
1762            let mut first_header_row = String::new();
1763            let mut second_header_row = String::new();
1764            let mut marker_header_row = String::new();
1765            for index in 0..self.term.columns() {
1766                let remainder = index % 10;
1767                if index > 0 && remainder == 0 {
1768                    first_header_row.push_str(&format!("{:>10}", (index / 10)));
1769                }
1770                second_header_row += &remainder.to_string();
1771                if index == self.expected_hyperlink.hovered_grid_point.column.0 {
1772                    marker_header_row.push('โ†“');
1773                } else {
1774                    marker_header_row.push(' ');
1775                }
1776            }
1777
1778            let remainder = (self.term.columns() - 1) % 10;
1779            if remainder != 0 {
1780                first_header_row.push_str(&" ".repeat(remainder));
1781            }
1782
1783            result += &format!("\n      [ {}]\n", first_header_row);
1784            result += &format!("      [{}]\n", second_header_row);
1785            result += &format!("       {}", marker_header_row);
1786
1787            for cell in self
1788                .term
1789                .renderable_content()
1790                .display_iter
1791                .filter(|cell| !cell.flags.intersects(WIDE_CHAR_SPACERS))
1792            {
1793                if cell.point.column.0 == 0 {
1794                    let prefix =
1795                        if cell.point.line == self.expected_hyperlink.hovered_grid_point.line {
1796                            'โ†’'
1797                        } else {
1798                            ' '
1799                        };
1800                    result += &format!("\n{prefix}[{:>3}] ", cell.point.line.to_string());
1801                }
1802
1803                match cell.c {
1804                    '\t' => result.push(' '),
1805                    c @ _ => result.push(c),
1806                }
1807            }
1808
1809            result
1810        }
1811    }
1812
1813    fn test_hyperlink<'a>(
1814        columns: usize,
1815        total_cells: usize,
1816        test_lines: impl Iterator<Item = &'a str>,
1817        hyperlink_kind: HyperlinkKind,
1818        source_location: &str,
1819    ) {
1820        const CARGO_DIR_REGEX: &str =
1821            r#"\s+(Compiling|Checking|Documenting) [^(]+\((?<link>(?<path>.+))\)"#;
1822        const RUST_DIAGNOSTIC_REGEX: &str = r#"\s+(-->|:::|at) (?<link>(?<path>.+?))(:$|$)"#;
1823        const ISSUE_12338_REGEX: &str =
1824            r#"[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2} (?<link>(?<path>.+))"#;
1825        const MULTIPLE_SAME_LINE_REGEX: &str =
1826            r#"(?<link>(?<path>๐Ÿฆ€ multiple_same_line ๐Ÿฆ€) ๐Ÿšฃ(?<line>[0-9]+) ๐Ÿ›(?<column>[0-9]+)):"#;
1827        const PATH_HYPERLINK_TIMEOUT_MS: u64 = 1000;
1828
1829        thread_local! {
1830            static TEST_REGEX_SEARCHES: RefCell<RegexSearches> =
1831                RefCell::new({
1832                    let default_settings_content: Rc<SettingsContent> =
1833                        settings::parse_json_with_comments(&settings::default_settings()).unwrap();
1834                    let default_terminal_settings = TerminalSettings::from_settings(&default_settings_content);
1835
1836                    RegexSearches::new([
1837                        RUST_DIAGNOSTIC_REGEX,
1838                        CARGO_DIR_REGEX,
1839                        ISSUE_12338_REGEX,
1840                        MULTIPLE_SAME_LINE_REGEX,
1841                    ]
1842                        .into_iter()
1843                        .chain(default_terminal_settings.path_hyperlink_regexes
1844                            .iter()
1845                            .map(AsRef::as_ref)),
1846                    PATH_HYPERLINK_TIMEOUT_MS)
1847                });
1848        }
1849
1850        let term_size = TermSize::new(columns, total_cells / columns + 2);
1851        let (term, expected_hyperlink) =
1852            build_term_from_test_lines(hyperlink_kind, term_size, test_lines);
1853        let hyperlink_found = TEST_REGEX_SEARCHES.with(|regex_searches| {
1854            find_from_grid_point(
1855                &term,
1856                expected_hyperlink.hovered_grid_point,
1857                &mut regex_searches.borrow_mut(),
1858                PathStyle::local(),
1859            )
1860        });
1861        let check_hyperlink_match =
1862            CheckHyperlinkMatch::new(&term, &expected_hyperlink, source_location);
1863        match hyperlink_found {
1864            Some((hyperlink_word, false, hyperlink_match)) => {
1865                check_hyperlink_match.check_path_with_position_and_match(
1866                    PathWithPosition::parse_str(&hyperlink_word),
1867                    &hyperlink_match,
1868                );
1869            }
1870            Some((hyperlink_word, true, hyperlink_match)) => {
1871                check_hyperlink_match.check_iri_and_match(hyperlink_word, &hyperlink_match);
1872            }
1873            None => {
1874                if expected_hyperlink.hyperlink_match.start()
1875                    != expected_hyperlink.hyperlink_match.end()
1876                {
1877                    assert!(
1878                        false,
1879                        "No hyperlink found\n     at {source_location}:\n{}",
1880                        check_hyperlink_match.format_renderable_content()
1881                    )
1882                }
1883            }
1884        }
1885    }
1886}