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
518 lines
16 KiB
Rust
518 lines
16 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use collections::HashMap;
|
|
use gpui::{AbsoluteLength, FontFeatures, SharedString, px};
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use settings_macros::{MergeFrom, with_fallible_options};
|
|
|
|
use crate::FontFamilyName;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
pub struct ProjectTerminalSettingsContent {
|
|
/// What shell to use when opening a terminal.
|
|
///
|
|
/// Default: system
|
|
pub shell: Option<Shell>,
|
|
/// What working directory to use when launching the terminal
|
|
///
|
|
/// Default: current_project_directory
|
|
pub working_directory: Option<WorkingDirectory>,
|
|
/// Any key-value pairs added to this list will be added to the terminal's
|
|
/// environment. Use `:` to separate multiple values.
|
|
///
|
|
/// Default: {}
|
|
pub env: Option<HashMap<String, String>>,
|
|
/// Activates the python virtual environment, if one is found, in the
|
|
/// terminal's working directory (as resolved by the working_directory
|
|
/// setting). Set this to "off" to disable this behavior.
|
|
///
|
|
/// 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]
|
|
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
pub struct TerminalSettingsContent {
|
|
#[serde(flatten)]
|
|
pub project: ProjectTerminalSettingsContent,
|
|
/// Sets the terminal's font size.
|
|
///
|
|
/// If this option is not included,
|
|
/// the terminal will default to matching the buffer's font size.
|
|
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
|
pub font_size: Option<f32>,
|
|
/// Sets the terminal's font family.
|
|
///
|
|
/// If this option is not included,
|
|
/// the terminal will default to matching the buffer's font family.
|
|
pub font_family: Option<FontFamilyName>,
|
|
|
|
/// Sets the terminal's font fallbacks.
|
|
///
|
|
/// If this option is not included,
|
|
/// the terminal will default to matching the buffer's font fallbacks.
|
|
#[schemars(extend("uniqueItems" = true))]
|
|
pub font_fallbacks: Option<Vec<FontFamilyName>>,
|
|
|
|
/// Sets the terminal's line height.
|
|
///
|
|
/// Default: comfortable
|
|
pub line_height: Option<TerminalLineHeight>,
|
|
pub font_features: Option<FontFeatures>,
|
|
/// Sets the terminal's font weight in CSS weight units 0-900.
|
|
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
|
pub font_weight: Option<f32>,
|
|
/// Default cursor shape for the terminal.
|
|
/// Can be "bar", "block", "underline", or "hollow".
|
|
///
|
|
/// Default: "block"
|
|
pub cursor_shape: Option<CursorShapeContent>,
|
|
/// Sets the cursor blinking behavior in the terminal.
|
|
///
|
|
/// Default: terminal_controlled
|
|
pub blinking: Option<TerminalBlink>,
|
|
/// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
|
|
/// Alternate Scroll mode converts mouse scroll events into up / down key
|
|
/// presses when in the alternate screen (e.g. when running applications
|
|
/// like vim or less). The terminal can still set and unset this mode.
|
|
///
|
|
/// Default: on
|
|
pub alternate_scroll: Option<AlternateScroll>,
|
|
/// Sets whether the option key behaves as the meta key.
|
|
///
|
|
/// Default: false
|
|
pub option_as_meta: Option<bool>,
|
|
/// Whether or not selecting text in the terminal will automatically
|
|
/// copy to the system clipboard.
|
|
///
|
|
/// Default: false
|
|
pub copy_on_select: Option<bool>,
|
|
/// Whether to keep the text selection after copying it to the clipboard.
|
|
///
|
|
/// Default: true
|
|
pub keep_selection_on_copy: Option<bool>,
|
|
/// Whether to show the terminal button in the status bar.
|
|
///
|
|
/// Default: true
|
|
pub button: Option<bool>,
|
|
pub dock: Option<TerminalDockPosition>,
|
|
/// Default width when the terminal is docked to the left or right.
|
|
///
|
|
/// Default: 640
|
|
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
|
pub default_width: Option<f32>,
|
|
/// Default height when the terminal is docked to the bottom.
|
|
///
|
|
/// Default: 320
|
|
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
|
pub default_height: Option<f32>,
|
|
/// The maximum number of lines to keep in the scrollback history.
|
|
/// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
|
|
/// 0 disables the scrolling.
|
|
/// Existing terminals will not pick up this change until they are recreated.
|
|
/// See <a href="https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213">Alacritty documentation</a> for more information.
|
|
///
|
|
/// Default: 10_000
|
|
pub max_scroll_history_lines: Option<usize>,
|
|
/// The multiplier for scrolling with the mouse wheel.
|
|
///
|
|
/// Default: 1.0
|
|
pub scroll_multiplier: Option<f32>,
|
|
/// Toolbar related settings
|
|
pub toolbar: Option<TerminalToolbarContent>,
|
|
/// Scrollbar-related settings
|
|
pub scrollbar: Option<ScrollbarSettingsContent>,
|
|
/// The minimum APCA perceptual contrast between foreground and background colors.
|
|
///
|
|
/// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
|
|
/// especially for dark mode. Values range from 0 to 106.
|
|
///
|
|
/// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
|
|
/// https://readtech.org/ARC/tests/bronze-simple-mode/
|
|
/// - 0: No contrast adjustment
|
|
/// - 45: Minimum for large fluent text (36px+)
|
|
/// - 60: Minimum for other content text
|
|
/// - 75: Minimum for body text
|
|
/// - 90: Preferred for body text
|
|
///
|
|
/// Default: 45
|
|
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
|
pub minimum_contrast: Option<f32>,
|
|
}
|
|
|
|
/// Shell configuration to open the terminal with.
|
|
#[derive(
|
|
Clone,
|
|
Debug,
|
|
Default,
|
|
Serialize,
|
|
Deserialize,
|
|
PartialEq,
|
|
Eq,
|
|
JsonSchema,
|
|
MergeFrom,
|
|
strum::EnumDiscriminants,
|
|
)]
|
|
#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Shell {
|
|
/// Use the system's default terminal configuration in /etc/passwd
|
|
#[default]
|
|
System,
|
|
/// Use a specific program with no arguments.
|
|
Program(String),
|
|
/// Use a specific program with arguments.
|
|
WithArguments {
|
|
/// The program to run.
|
|
program: String,
|
|
/// The arguments to pass to the program.
|
|
args: Vec<String>,
|
|
/// An optional string to override the title of the terminal tab
|
|
title_override: Option<SharedString>,
|
|
},
|
|
}
|
|
|
|
#[derive(
|
|
Clone,
|
|
Debug,
|
|
Serialize,
|
|
Deserialize,
|
|
PartialEq,
|
|
Eq,
|
|
JsonSchema,
|
|
MergeFrom,
|
|
strum::EnumDiscriminants,
|
|
)]
|
|
#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum WorkingDirectory {
|
|
/// Use the current file's project directory. Will Fallback to the
|
|
/// first project directory strategy if unsuccessful.
|
|
CurrentProjectDirectory,
|
|
/// Use the first project in this workspace's directory.
|
|
FirstProjectDirectory,
|
|
/// Always use this platform's home directory (if it can be found).
|
|
AlwaysHome,
|
|
/// Always use a specific directory. This value will be shell expanded.
|
|
/// If this path is not a valid directory the terminal will default to
|
|
/// this platform's home directory (if it can be found).
|
|
Always { directory: String },
|
|
}
|
|
|
|
#[with_fallible_options]
|
|
#[derive(
|
|
Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
|
|
)]
|
|
pub struct ScrollbarSettingsContent {
|
|
/// When to show the scrollbar in the terminal.
|
|
///
|
|
/// Default: inherits editor scrollbar settings
|
|
pub show: Option<ShowScrollbar>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TerminalLineHeight {
|
|
/// Use a line height that's comfortable for reading, 1.618
|
|
#[default]
|
|
Comfortable,
|
|
/// Use a standard line height, 1.3. This option is useful for TUIs,
|
|
/// particularly if they use box characters
|
|
Standard,
|
|
/// Use a custom line height.
|
|
Custom(#[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] f32),
|
|
}
|
|
|
|
impl TerminalLineHeight {
|
|
pub fn value(&self) -> AbsoluteLength {
|
|
let value = match self {
|
|
TerminalLineHeight::Comfortable => 1.618,
|
|
TerminalLineHeight::Standard => 1.3,
|
|
TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
|
|
};
|
|
px(value).into()
|
|
}
|
|
}
|
|
|
|
/// When to show the scrollbar.
|
|
///
|
|
/// Default: auto
|
|
#[derive(
|
|
Copy,
|
|
Clone,
|
|
Debug,
|
|
Default,
|
|
Serialize,
|
|
Deserialize,
|
|
JsonSchema,
|
|
MergeFrom,
|
|
PartialEq,
|
|
Eq,
|
|
strum::VariantArray,
|
|
strum::VariantNames,
|
|
)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ShowScrollbar {
|
|
/// Show the scrollbar if there's important information or
|
|
/// follow the system's configured behavior.
|
|
#[default]
|
|
Auto,
|
|
/// Match the system's configured behavior.
|
|
System,
|
|
/// Always show the scrollbar.
|
|
Always,
|
|
/// Never show the scrollbar.
|
|
Never,
|
|
}
|
|
|
|
#[derive(
|
|
Clone,
|
|
Copy,
|
|
Debug,
|
|
Default,
|
|
Serialize,
|
|
Deserialize,
|
|
PartialEq,
|
|
Eq,
|
|
JsonSchema,
|
|
MergeFrom,
|
|
strum::VariantArray,
|
|
strum::VariantNames,
|
|
)]
|
|
#[serde(rename_all = "snake_case")]
|
|
// todo() -> combine with CursorShape
|
|
pub enum CursorShapeContent {
|
|
/// 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,
|
|
}
|
|
|
|
#[derive(
|
|
Copy,
|
|
Clone,
|
|
Debug,
|
|
Serialize,
|
|
Deserialize,
|
|
PartialEq,
|
|
Eq,
|
|
JsonSchema,
|
|
MergeFrom,
|
|
strum::VariantArray,
|
|
strum::VariantNames,
|
|
)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TerminalBlink {
|
|
/// Never blink the cursor, ignoring the terminal mode.
|
|
Off,
|
|
/// Default the cursor blink to off, but allow the terminal to
|
|
/// set blinking.
|
|
TerminalControlled,
|
|
/// Always blink the cursor, ignoring the terminal mode.
|
|
On,
|
|
}
|
|
|
|
#[derive(
|
|
Clone,
|
|
Copy,
|
|
Debug,
|
|
Serialize,
|
|
Deserialize,
|
|
PartialEq,
|
|
Eq,
|
|
JsonSchema,
|
|
MergeFrom,
|
|
strum::VariantArray,
|
|
strum::VariantNames,
|
|
)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum AlternateScroll {
|
|
On,
|
|
Off,
|
|
}
|
|
|
|
// Toolbar related settings
|
|
#[with_fallible_options]
|
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
|
|
pub struct TerminalToolbarContent {
|
|
/// Whether to display the terminal title in breadcrumbs inside the terminal pane.
|
|
/// Only shown if the terminal title is not empty.
|
|
///
|
|
/// The shell running in the terminal needs to be configured to emit the title.
|
|
/// Example: `echo -e "\e]2;New Title\007";`
|
|
///
|
|
/// Default: true
|
|
pub breadcrumbs: Option<bool>,
|
|
}
|
|
|
|
#[derive(
|
|
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
|
|
)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum CondaManager {
|
|
/// Automatically detect the conda manager
|
|
#[default]
|
|
Auto,
|
|
/// Use conda
|
|
Conda,
|
|
/// Use mamba
|
|
Mamba,
|
|
/// Use micromamba
|
|
Micromamba,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum VenvSettings {
|
|
#[default]
|
|
Off,
|
|
On {
|
|
/// Default directories to search for virtual environments, relative
|
|
/// to the current working directory. We recommend overriding this
|
|
/// in your project's settings, rather than globally.
|
|
activate_script: Option<ActivateScript>,
|
|
venv_name: Option<String>,
|
|
directories: Option<Vec<PathBuf>>,
|
|
/// Preferred Conda manager to use when activating Conda environments.
|
|
///
|
|
/// Default: auto
|
|
conda_manager: Option<CondaManager>,
|
|
},
|
|
}
|
|
#[with_fallible_options]
|
|
pub struct VenvSettingsContent<'a> {
|
|
pub activate_script: ActivateScript,
|
|
pub venv_name: &'a str,
|
|
pub directories: &'a [PathBuf],
|
|
pub conda_manager: CondaManager,
|
|
}
|
|
|
|
impl VenvSettings {
|
|
pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
|
|
match self {
|
|
VenvSettings::Off => None,
|
|
VenvSettings::On {
|
|
activate_script,
|
|
venv_name,
|
|
directories,
|
|
conda_manager,
|
|
} => Some(VenvSettingsContent {
|
|
activate_script: activate_script.unwrap_or(ActivateScript::Default),
|
|
venv_name: venv_name.as_deref().unwrap_or(""),
|
|
directories: directories.as_deref().unwrap_or(&[]),
|
|
conda_manager: conda_manager.unwrap_or(CondaManager::Auto),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
|
|
#[serde(untagged)]
|
|
pub enum PathHyperlinkRegex {
|
|
SingleLine(String),
|
|
MultiLine(Vec<String>),
|
|
}
|
|
|
|
#[derive(
|
|
Copy,
|
|
Clone,
|
|
Debug,
|
|
Serialize,
|
|
Deserialize,
|
|
JsonSchema,
|
|
MergeFrom,
|
|
PartialEq,
|
|
Eq,
|
|
strum::VariantArray,
|
|
strum::VariantNames,
|
|
)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TerminalDockPosition {
|
|
Left,
|
|
Bottom,
|
|
Right,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ActivateScript {
|
|
#[default]
|
|
Default,
|
|
Csh,
|
|
Fish,
|
|
Nushell,
|
|
PowerShell,
|
|
Pyenv,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use serde_json::json;
|
|
|
|
use crate::{ProjectSettingsContent, Shell, UserSettingsContent};
|
|
|
|
#[test]
|
|
fn test_project_settings() {
|
|
let project_content =
|
|
json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true});
|
|
|
|
let user_content =
|
|
json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false});
|
|
|
|
let user_settings = serde_json::from_value::<UserSettingsContent>(user_content).unwrap();
|
|
let project_settings =
|
|
serde_json::from_value::<ProjectSettingsContent>(project_content).unwrap();
|
|
|
|
assert_eq!(
|
|
user_settings.content.terminal.unwrap().project.shell,
|
|
Some(Shell::Program("/bin/user".to_owned()))
|
|
);
|
|
assert_eq!(user_settings.content.project.terminal, None);
|
|
assert_eq!(
|
|
project_settings.terminal.unwrap().shell,
|
|
Some(Shell::Program("/bin/project".to_owned()))
|
|
);
|
|
}
|
|
}
|