terminal: New settings for path hyperlink regexes (#40305)

Closes:
- #12338
- #40202 

1. Adds two new settings which allow customizing the set of regexes used
to identify path hyperlinks in terminal
1. Fixes path hyperlinks for paths containing unicode emoji and
punctuation, for example, `mojo.🔥`
1. Fixes path hyperlinks for Windows verbatim paths, for example,
`\\?\C:\Over\here.rs`.
1. Improves path hyperlink performance, especially for terminals with a
lot of content
1. Replaces existing custom hard-coded default path hyperlink parsing
logic with a set of customizable default regexes

## New settings

(from default.json)

### terminal.path_hyperlink_regexes

Regexes used to identify paths for hyperlink navigation. Supports
optional named capture
groups `path`, `line`, `column`, and `link`. If none of these are
present, the entire match
is the hyperlink target. If `path` is present, it is the hyperlink
target, along with `line`
and `column` if present. `link` may be used to customize what text in
terminal is part of the
hyperlink. If `link` is not present, the text of the entire match is
used. If `line` and
`column` are not present, the default built-in line and column suffix
processing is used
which parses `line:column` and `(line,column)` variants. The default
value handles Python
diagnostics and common path, line, column syntaxes. This can be extended
or replaced to
handle specific scenarios. For example, to enable support for
hyperlinking paths which
contain spaces in rust output,
```
[
  "\\s+(-->|:::|at) (?<link>(?<path>.+?))(:$|$)",
  "\\s+(Compiling|Checking|Documenting) [^(]+\\((?<link>(?<path>.+))\\)"
],
```
could be used. Processing stops at the first regex with a match, even if
no link is
produced which is the case when the cursor is not over the hyperlinked
text. For best
performance it is recommended to order regexes from most common to least
common. For
readability and documentation, each regex may be an array of strings
which are collected
into one multi-line regex string for use in terminal path hyperlink
detection.

### terminal.path_hyperlink_timeout_ms
Timeout for hover and Cmd-click path hyperlink discovery in
milliseconds. Specifying a
timeout of `0` will disable path hyperlinking in terminal.

## Performance

This PR fixes terminal to only search the hovered line for hyperlinks
and adds a benchmark. Before this fix, hyperlink detection grows
linearly with terminal content, with this fix it is proportional only to
the hovered line. The gains come from replacing
`visible_regex_match_iter`, which searched all visible lines, with code
that only searches the line hovered on (including if the line is
wrapped).

Local benchmark timings (terminal with 500 lines of content):

||main|this PR|Δ|
|-|-|-:|-|
| cargo_hyperlink_benchmark | 1.4 ms | 13 µs | -99.0% |
| rust_hyperlink_benchmark | 1.2 ms | 11 µs | -99.1% |
| ls_hyperlink_benchmark | 1.3 ms | 7 µs |  -99.5% |

Release Notes:

- terminal: New settings to allow customizing the set of regexes used to
identify path hyperlinks in terminal
- terminal: Fixed terminal path hyperlinks for paths containing unicode
punctuation and emoji, e.g. mojo.🔥
- terminal: Fixed path hyperlinks for Windows verbatim paths, for
example, `\\?\C:\Over\here.rs`
- terminal: Improved terminal hyperlink performance, especially for
terminals with a lot of content visible
This commit is contained in:
Dave Waggoner
2025-11-21 14:01:06 -05:00
committed by GitHub
parent 0492255d7b
commit e76b485de3
10 changed files with 768 additions and 285 deletions
Generated
+3 -1
View File
@@ -17072,16 +17072,17 @@ dependencies = [
"alacritty_terminal",
"anyhow",
"collections",
"fancy-regex 0.14.0",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
"libc",
"log",
"rand 0.9.2",
"regex",
"release_channel",
"schemars",
"serde",
"serde_json",
"settings",
"smol",
"sysinfo 0.37.2",
@@ -17091,6 +17092,7 @@ dependencies = [
"url",
"urlencoding",
"util",
"util_macros",
"windows 0.61.3",
]
+53 -1
View File
@@ -1585,7 +1585,59 @@
//
// Most terminal themes have APCA values of 40-70.
// A value of 45 preserves colorful themes while ensuring legibility.
"minimum_contrast": 45
"minimum_contrast": 45,
// Regexes used to identify paths for hyperlink navigation. Supports optional named capture
// groups `path`, `line`, `column`, and `link`. If none of these are present, the entire match
// is the hyperlink target. If `path` is present, it is the hyperlink target, along with `line`
// and `column` if present. `link` may be used to customize what text in terminal is part of the
// hyperlink. If `link` is not present, the text of the entire match is used. If `line` and
// `column` are not present, the default built-in line and column suffix processing is used
// which parses `line:column` and `(line,column)` variants. The default value handles Python
// diagnostics and common path, line, column syntaxes. This can be extended or replaced to
// handle specific scenarios. For example, to enable support for hyperlinking paths which
// contain spaces in rust output,
//
// [
// "\\s+(-->|:::|at) (?<link>(?<path>.+?))(:$|$)",
// "\\s+(Compiling|Checking|Documenting) [^(]+\\((?<link>(?<path>.+))\\)"
// ],
//
// could be used. Processing stops at the first regex with a match, even if no link is
// produced which is the case when the cursor is not over the hyperlinked text. For best
// performance it is recommended to order regexes from most common to least common. For
// readability and documentation, each regex may be an array of strings which are collected
// into one multi-line regex string for use in terminal path hyperlink detection.
"path_hyperlink_regexes": [
// Python-style diagnostics
"File \"(?<path>[^\"]+)\", line (?<line>[0-9]+)",
// Common path syntax with optional line, column, description, trailing punctuation, or
// surrounding symbols or quotes
[
"(?x)",
"# optionally starts with 0-2 opening prefix symbols",
"[({\\[<]{0,2}",
"# which may be followed by an opening quote",
"(?<quote>[\"'`])?",
"# `path` is the shortest sequence of any non-space character",
"(?<link>(?<path>[^ ]+?",
" # which may end with a line and optionally a column,",
" (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
"))",
"# which must be followed by a matching quote",
"(?(<quote>)\\k<quote>)",
"# and optionally a single closing symbol",
"[)}\\]>]?",
"# if line/column matched, may be followed by a description",
"(?(<line_column>):[^ 0-9][^ ]*)?",
"# which may be followed by trailing punctuation",
"[.,:)}\\]>]*",
"# and always includes trailing whitespace or end of line",
"([ ]+|$)"
]
],
// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
// timeout of `0` will disable path hyperlinking in terminal.
"path_hyperlink_timeout_ms": 1
},
"code_actions_on_format": {},
// Settings related to running tasks.
+4
View File
@@ -240,6 +240,8 @@ impl Project {
settings.cursor_shape,
settings.alternate_scroll,
settings.max_scroll_history_lines,
settings.path_hyperlink_regexes,
settings.path_hyperlink_timeout_ms,
is_via_remote,
cx.entity_id().as_u64(),
Some(completion_tx),
@@ -369,6 +371,8 @@ impl Project {
settings.cursor_shape,
settings.alternate_scroll,
settings.max_scroll_history_lines,
settings.path_hyperlink_regexes,
settings.path_hyperlink_timeout_ms,
is_via_remote,
cx.entity_id().as_u64(),
None,
@@ -29,6 +29,41 @@ pub struct ProjectTerminalSettingsContent {
///
/// Default: on
pub detect_venv: Option<VenvSettings>,
/// Regexes used to identify paths for hyperlink navigation.
///
/// Default: [
/// // Python-style diagnostics
/// "File \"(?<path>[^\"]+)\", line (?<line>[0-9]+)",
/// // Common path syntax with optional line, column, description, trailing punctuation, or
/// // surrounding symbols or quotes
/// [
/// "(?x)",
/// "# optionally starts with 0-2 opening prefix symbols",
/// "[({\\[<]{0,2}",
/// "# which may be followed by an opening quote",
/// "(?<quote>[\"'`])?",
/// "# `path` is the shortest sequence of any non-space character",
/// "(?<link>(?<path>[^ ]+?",
/// " # which may end with a line and optionally a column,",
/// " (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
/// "))",
/// "# which must be followed by a matching quote",
/// "(?(<quote>)\\k<quote>)",
/// "# and optionally a single closing symbol",
/// "[)}\\]>]?",
/// "# if line/column matched, may be followed by a description",
/// "(?(<line_column>):[^ 0-9][^ ]*)?",
/// "# which may be followed by trailing punctuation",
/// "[.,:)}\\]>]*",
/// "# and always includes trailing whitespace or end of line",
/// "([ ]+|$)"
/// ]
/// ]
pub path_hyperlink_regexes: Option<Vec<PathHyperlinkRegex>>,
/// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds.
///
/// Default: 1
pub path_hyperlink_timeout_ms: Option<u64>,
}
#[with_fallible_options]
@@ -412,6 +447,13 @@ impl VenvSettings {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
#[serde(untagged)]
pub enum PathHyperlinkRegex {
SingleLine(String),
MultiLine(Vec<String>),
}
#[derive(
Copy,
Clone,
+2
View File
@@ -772,6 +772,8 @@ impl VsCodeSettings {
working_directory: None,
env,
detect_venv: None,
path_hyperlink_regexes: None,
path_hyperlink_timeout_ms: None,
}
}
+5 -2
View File
@@ -25,6 +25,7 @@ anyhow.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
libc.workspace = true
log.workspace = true
release_channel.workspace = true
@@ -37,9 +38,8 @@ task.workspace = true
theme.workspace = true
thiserror.workspace = true
util.workspace = true
regex.workspace = true
fancy-regex.workspace = true
urlencoding.workspace = true
itertools.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
@@ -47,4 +47,7 @@ windows.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json.workspace = true
settings = { workspace = true, features = ["test-support"] }
url.workspace = true
util_macros.workspace = true
+21 -2
View File
@@ -374,7 +374,7 @@ impl TerminalBuilder {
scroll_px: px(0.),
next_link_id: 0,
selection_phase: SelectionPhase::Ended,
hyperlink_regex_searches: RegexSearches::new(),
hyperlink_regex_searches: RegexSearches::default(),
vi_mode_enabled: false,
is_remote_terminal: false,
last_mouse_move_time: Instant::now(),
@@ -388,6 +388,8 @@ impl TerminalBuilder {
cursor_shape,
alternate_scroll,
max_scroll_history_lines,
path_hyperlink_regexes: Vec::default(),
path_hyperlink_timeout_ms: 0,
window_id,
},
child_exited: None,
@@ -408,6 +410,8 @@ impl TerminalBuilder {
cursor_shape: CursorShape,
alternate_scroll: AlternateScroll,
max_scroll_history_lines: Option<usize>,
path_hyperlink_regexes: Vec<String>,
path_hyperlink_timeout_ms: u64,
is_remote_terminal: bool,
window_id: u64,
completion_tx: Option<Sender<Option<ExitStatus>>>,
@@ -592,7 +596,10 @@ impl TerminalBuilder {
scroll_px: px(0.),
next_link_id: 0,
selection_phase: SelectionPhase::Ended,
hyperlink_regex_searches: RegexSearches::new(),
hyperlink_regex_searches: RegexSearches::new(
&path_hyperlink_regexes,
path_hyperlink_timeout_ms,
),
vi_mode_enabled: false,
is_remote_terminal,
last_mouse_move_time: Instant::now(),
@@ -606,6 +613,8 @@ impl TerminalBuilder {
cursor_shape,
alternate_scroll,
max_scroll_history_lines,
path_hyperlink_regexes,
path_hyperlink_timeout_ms,
window_id,
},
child_exited: None,
@@ -838,6 +847,8 @@ struct CopyTemplate {
cursor_shape: CursorShape,
alternate_scroll: AlternateScroll,
max_scroll_history_lines: Option<usize>,
path_hyperlink_regexes: Vec<String>,
path_hyperlink_timeout_ms: u64,
window_id: u64,
}
@@ -2163,6 +2174,8 @@ impl Terminal {
self.template.cursor_shape,
self.template.alternate_scroll,
self.template.max_scroll_history_lines,
self.template.path_hyperlink_regexes.clone(),
self.template.path_hyperlink_timeout_ms,
self.is_remote_terminal,
self.template.window_id,
None,
@@ -2404,6 +2417,8 @@ mod tests {
CursorShape::default(),
AlternateScroll::On,
None,
vec![],
0,
false,
0,
Some(completion_tx),
@@ -2452,6 +2467,8 @@ mod tests {
CursorShape::default(),
AlternateScroll::On,
None,
vec![],
0,
false,
0,
Some(completion_tx),
@@ -2527,6 +2544,8 @@ mod tests {
CursorShape::default(),
AlternateScroll::On,
None,
Vec::new(),
0,
false,
0,
Some(completion_tx),
File diff suppressed because it is too large Load Diff
+14 -2
View File
@@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize};
pub use settings::AlternateScroll;
use settings::{
RegisterSetting, ShowScrollbar, TerminalBlink, TerminalDockPosition, TerminalLineHeight,
VenvSettings, WorkingDirectory, merge_from::MergeFrom,
PathHyperlinkRegex, RegisterSetting, ShowScrollbar, TerminalBlink, TerminalDockPosition,
TerminalLineHeight, VenvSettings, WorkingDirectory, merge_from::MergeFrom,
};
use task::Shell;
use theme::FontFamilyName;
@@ -47,6 +47,8 @@ pub struct TerminalSettings {
pub toolbar: Toolbar,
pub scrollbar: ScrollbarSettings,
pub minimum_contrast: f32,
pub path_hyperlink_regexes: Vec<String>,
pub path_hyperlink_timeout_ms: u64,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -116,6 +118,16 @@ impl settings::Settings for TerminalSettings {
show: user_content.scrollbar.unwrap().show,
},
minimum_contrast: user_content.minimum_contrast.unwrap(),
path_hyperlink_regexes: project_content
.path_hyperlink_regexes
.unwrap()
.into_iter()
.map(|regex| match regex {
PathHyperlinkRegex::SingleLine(regex) => regex,
PathHyperlinkRegex::MultiLine(regex) => regex.join("\n"),
})
.collect(),
path_hyperlink_timeout_ms: project_content.path_hyperlink_timeout_ms.unwrap(),
}
}
}
+47
View File
@@ -4115,6 +4115,53 @@ Example command to set the title: `echo -e "\e]2;New Title\007";`
}
```
### Terminal: Path Hyperlink Regexes
- Description: Regexes used to identify path hyperlinks. The regexes can be specified in two forms - a single regex string, or an array of strings (which will be collected into a single multi-line regex string).
- Setting: `path_hyperlink_regexes`
- Default:
```json [settings]
{
"terminal": {
"path_hyperlink_regexes": [
// Python-style diagnostics
"File \"(?<path>[^\"]+)\", line (?<line>[0-9]+)",
// Common path syntax with optional line, column, description, trailing punctuation, or
// surrounding symbols or quotes
[
"(?x)",
"# optionally starts with 0-2 opening prefix symbols",
"[({\\[<]{0,2}",
"# which may be followed by an opening quote",
"(?<quote>[\"'`])?",
"# `path` is the shortest sequence of any non-space character",
"(?<link>(?<path>[^ ]+?",
" # which may end with a line and optionally a column,",
" (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
"))",
"# which must be followed by a matching quote",
"(?(<quote>)\\k<quote>)",
"# and optionally a single closing symbol",
"[)}\\]>]?",
"# if line/column matched, may be followed by a description",
"(?(<line_column>):[^ 0-9][^ ]*)?",
"# which may be followed by trailing punctuation",
"[.,:)}\\]>]*",
"# and always includes trailing whitespace or end of line",
"([ ]+|$)"
]
]
}
}
```
### Terminal: Path Hyperlink Timeout (ms)
- Description: Maximum time to search for a path hyperlink. When set to 0, path hyperlinks are disabled.
- Setting: `path_hyperlink_timeout_ms`
- Default: `1`
## REPL
- Description: Repl settings.