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:
Generated
+3
-1
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -772,6 +772,8 @@ impl VsCodeSettings {
|
||||
working_directory: None,
|
||||
env,
|
||||
detect_venv: None,
|
||||
path_hyperlink_regexes: None,
|
||||
path_hyperlink_timeout_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user