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
179 lines
6.6 KiB
Rust
179 lines
6.6 KiB
Rust
use alacritty_terminal::vte::ansi::{
|
|
CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
|
|
};
|
|
use collections::HashMap;
|
|
use gpui::{FontFallbacks, FontFeatures, FontWeight, Pixels, px};
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
pub use settings::AlternateScroll;
|
|
|
|
use settings::{
|
|
PathHyperlinkRegex, RegisterSetting, ShowScrollbar, TerminalBlink, TerminalDockPosition,
|
|
TerminalLineHeight, VenvSettings, WorkingDirectory, merge_from::MergeFrom,
|
|
};
|
|
use task::Shell;
|
|
use theme::FontFamilyName;
|
|
|
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
|
pub struct Toolbar {
|
|
pub breadcrumbs: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, RegisterSetting)]
|
|
pub struct TerminalSettings {
|
|
pub shell: Shell,
|
|
pub working_directory: WorkingDirectory,
|
|
pub font_size: Option<Pixels>, // todo(settings_refactor) can be non-optional...
|
|
pub font_family: Option<FontFamilyName>,
|
|
pub font_fallbacks: Option<FontFallbacks>,
|
|
pub font_features: Option<FontFeatures>,
|
|
pub font_weight: Option<FontWeight>,
|
|
pub line_height: TerminalLineHeight,
|
|
pub env: HashMap<String, String>,
|
|
pub cursor_shape: CursorShape,
|
|
pub blinking: TerminalBlink,
|
|
pub alternate_scroll: AlternateScroll,
|
|
pub option_as_meta: bool,
|
|
pub copy_on_select: bool,
|
|
pub keep_selection_on_copy: bool,
|
|
pub button: bool,
|
|
pub dock: TerminalDockPosition,
|
|
pub default_width: Pixels,
|
|
pub default_height: Pixels,
|
|
pub detect_venv: VenvSettings,
|
|
pub max_scroll_history_lines: Option<usize>,
|
|
pub scroll_multiplier: f32,
|
|
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)]
|
|
pub struct ScrollbarSettings {
|
|
/// When to show the scrollbar in the terminal.
|
|
///
|
|
/// Default: inherits editor scrollbar settings
|
|
pub show: Option<ShowScrollbar>,
|
|
}
|
|
|
|
fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
|
|
match shell {
|
|
settings::Shell::System => Shell::System,
|
|
settings::Shell::Program(program) => Shell::Program(program),
|
|
settings::Shell::WithArguments {
|
|
program,
|
|
args,
|
|
title_override,
|
|
} => Shell::WithArguments {
|
|
program,
|
|
args,
|
|
title_override: title_override.map(Into::into),
|
|
},
|
|
}
|
|
}
|
|
|
|
impl settings::Settings for TerminalSettings {
|
|
fn from_settings(content: &settings::SettingsContent) -> Self {
|
|
let user_content = content.terminal.clone().unwrap();
|
|
// Note: we allow a subset of "terminal" settings in the project files.
|
|
let mut project_content = user_content.project.clone();
|
|
project_content.merge_from_option(content.project.terminal.as_ref());
|
|
TerminalSettings {
|
|
shell: settings_shell_to_task_shell(project_content.shell.unwrap()),
|
|
working_directory: project_content.working_directory.unwrap(),
|
|
font_size: user_content.font_size.map(px),
|
|
font_family: user_content.font_family,
|
|
font_fallbacks: user_content.font_fallbacks.map(|fallbacks| {
|
|
FontFallbacks::from_fonts(
|
|
fallbacks
|
|
.into_iter()
|
|
.map(|family| family.0.to_string())
|
|
.collect(),
|
|
)
|
|
}),
|
|
font_features: user_content.font_features,
|
|
font_weight: user_content.font_weight.map(FontWeight),
|
|
line_height: user_content.line_height.unwrap(),
|
|
env: project_content.env.unwrap(),
|
|
cursor_shape: user_content.cursor_shape.unwrap().into(),
|
|
blinking: user_content.blinking.unwrap(),
|
|
alternate_scroll: user_content.alternate_scroll.unwrap(),
|
|
option_as_meta: user_content.option_as_meta.unwrap(),
|
|
copy_on_select: user_content.copy_on_select.unwrap(),
|
|
keep_selection_on_copy: user_content.keep_selection_on_copy.unwrap(),
|
|
button: user_content.button.unwrap(),
|
|
dock: user_content.dock.unwrap(),
|
|
default_width: px(user_content.default_width.unwrap()),
|
|
default_height: px(user_content.default_height.unwrap()),
|
|
detect_venv: project_content.detect_venv.unwrap(),
|
|
scroll_multiplier: user_content.scroll_multiplier.unwrap(),
|
|
max_scroll_history_lines: user_content.max_scroll_history_lines,
|
|
toolbar: Toolbar {
|
|
breadcrumbs: user_content.toolbar.unwrap().breadcrumbs.unwrap(),
|
|
},
|
|
scrollbar: ScrollbarSettings {
|
|
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(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum CursorShape {
|
|
/// Cursor is a block like `█`.
|
|
#[default]
|
|
Block,
|
|
/// Cursor is an underscore like `_`.
|
|
Underline,
|
|
/// Cursor is a vertical bar like `⎸`.
|
|
Bar,
|
|
/// Cursor is a hollow box like `▯`.
|
|
Hollow,
|
|
}
|
|
|
|
impl From<settings::CursorShapeContent> for CursorShape {
|
|
fn from(value: settings::CursorShapeContent) -> Self {
|
|
match value {
|
|
settings::CursorShapeContent::Block => CursorShape::Block,
|
|
settings::CursorShapeContent::Underline => CursorShape::Underline,
|
|
settings::CursorShapeContent::Bar => CursorShape::Bar,
|
|
settings::CursorShapeContent::Hollow => CursorShape::Hollow,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<CursorShape> for AlacCursorShape {
|
|
fn from(value: CursorShape) -> Self {
|
|
match value {
|
|
CursorShape::Block => AlacCursorShape::Block,
|
|
CursorShape::Underline => AlacCursorShape::Underline,
|
|
CursorShape::Bar => AlacCursorShape::Beam,
|
|
CursorShape::Hollow => AlacCursorShape::HollowBlock,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<CursorShape> for AlacCursorStyle {
|
|
fn from(value: CursorShape) -> Self {
|
|
AlacCursorStyle {
|
|
shape: value.into(),
|
|
blinking: false,
|
|
}
|
|
}
|
|
}
|