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