Cargo.lock 🔗
@@ -5482,11 +5482,13 @@ dependencies = [
"futures",
"gpui",
"itertools",
+ "lazy_static",
"libc",
"mio-extras",
"ordered-float",
"procinfo",
"project",
+ "rand 0.8.5",
"settings",
"shellexpand",
"smallvec",
Mikayla Maki created
Tracking PR for Terminal hyperlinks
Cargo.lock | 2
crates/gpui/src/elements/overlay.rs | 29 +
crates/gpui/src/elements/tooltip.rs | 8
crates/terminal/Cargo.toml | 3
crates/terminal/src/mappings/mouse.rs | 6
crates/terminal/src/terminal.rs | 430 +++++++++++++--
crates/terminal/src/terminal_container_view.rs | 5
crates/terminal/src/terminal_element.rs | 156 ++++-
crates/terminal/src/terminal_view.rs | 4
crates/terminal/src/tests/terminal_test_context.rs | 40 +
10 files changed, 573 insertions(+), 110 deletions(-)
@@ -5482,11 +5482,13 @@ dependencies = [
"futures",
"gpui",
"itertools",
+ "lazy_static",
"libc",
"mio-extras",
"ordered-float",
"procinfo",
"project",
+ "rand 0.8.5",
"settings",
"shellexpand",
"smallvec",
@@ -14,6 +14,7 @@ pub struct Overlay {
anchor_position: Option<Vector2F>,
anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
+ position_mode: OverlayPositionMode,
hoverable: bool,
}
@@ -24,6 +25,12 @@ pub enum OverlayFitMode {
None,
}
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum OverlayPositionMode {
+ Window,
+ Local,
+}
+
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AnchorCorner {
TopLeft,
@@ -73,6 +80,7 @@ impl Overlay {
anchor_position: None,
anchor_corner: AnchorCorner::TopLeft,
fit_mode: OverlayFitMode::None,
+ position_mode: OverlayPositionMode::Window,
hoverable: false,
}
}
@@ -92,6 +100,11 @@ impl Overlay {
self
}
+ pub fn with_position_mode(mut self, position_mode: OverlayPositionMode) -> Self {
+ self.position_mode = position_mode;
+ self
+ }
+
pub fn with_hoverable(mut self, hoverable: bool) -> Self {
self.hoverable = hoverable;
self
@@ -123,8 +136,20 @@ impl Element for Overlay {
size: &mut Self::LayoutState,
cx: &mut PaintContext,
) {
- let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
- let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size);
+ let (anchor_position, mut bounds) = match self.position_mode {
+ OverlayPositionMode::Window => {
+ let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
+ let bounds = self.anchor_corner.get_bounds(anchor_position, *size);
+ (anchor_position, bounds)
+ }
+ OverlayPositionMode::Local => {
+ let anchor_position = self.anchor_position.unwrap_or_default();
+ let bounds = self
+ .anchor_corner
+ .get_bounds(bounds.origin() + anchor_position, *size);
+ (anchor_position, bounds)
+ }
+ };
match self.fit_mode {
OverlayFitMode::SnapToWindow => {
@@ -36,10 +36,10 @@ struct TooltipState {
#[derive(Clone, Deserialize, Default)]
pub struct TooltipStyle {
#[serde(flatten)]
- container: ContainerStyle,
- text: TextStyle,
+ pub container: ContainerStyle,
+ pub text: TextStyle,
keystroke: KeystrokeStyle,
- max_text_width: f32,
+ pub max_text_width: f32,
}
#[derive(Clone, Deserialize, Default)]
@@ -126,7 +126,7 @@ impl Tooltip {
}
}
- fn render_tooltip(
+ pub fn render_tooltip(
text: String,
style: TooltipStyle,
action: Option<Box<dyn Action>>,
@@ -29,6 +29,8 @@ shellexpand = "2.1.0"
libc = "0.2"
anyhow = "1"
thiserror = "1.0"
+lazy_static = "1.4.0"
+
[dev-dependencies]
@@ -36,3 +38,4 @@ gpui = { path = "../gpui", features = ["test-support"] }
client = { path = "../client", features = ["test-support"]}
project = { path = "../project", features = ["test-support"]}
workspace = { path = "../workspace", features = ["test-support"] }
+rand = "0.8.5"
@@ -202,7 +202,7 @@ pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal::
}
}
-pub fn mouse_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
+pub fn grid_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
let col = pos.x() / cur_size.cell_width;
let col = min(GridCol(col as usize), cur_size.last_column());
let line = pos.y() / cur_size.line_height;
@@ -295,7 +295,7 @@ fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String {
#[cfg(test)]
mod test {
- use crate::mappings::mouse::mouse_point;
+ use crate::mappings::mouse::grid_point;
#[test]
fn test_mouse_to_selection() {
@@ -317,7 +317,7 @@ mod test {
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let mouse_pos = mouse_pos - origin;
- let point = mouse_point(mouse_pos, cur_size, 0);
+ let point = grid_point(mouse_pos, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
@@ -29,18 +29,22 @@ use futures::{
};
use mappings::mouse::{
- alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
+ alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
};
use procinfo::LocalProcessInfo;
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
+use util::ResultExt;
use std::{
+ cmp::min,
collections::{HashMap, VecDeque},
fmt::Display,
+ io,
ops::{Deref, RangeInclusive, Sub},
- os::unix::prelude::AsRawFd,
+ os::unix::{prelude::AsRawFd, process::CommandExt},
path::PathBuf,
+ process::Command,
sync::Arc,
time::{Duration, Instant},
};
@@ -59,6 +63,7 @@ use crate::mappings::{
colors::{get_color_at_index, to_alac_rgb},
keys::to_esc_str,
};
+use lazy_static::lazy_static;
///Initialize and register all of our action handlers
pub fn init(cx: &mut MutableAppContext) {
@@ -70,12 +75,18 @@ pub fn init(cx: &mut MutableAppContext) {
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
const SCROLL_MULTIPLIER: f32 = 4.;
-// const MAX_SEARCH_LINES: usize = 100;
+const MAX_SEARCH_LINES: usize = 100;
const DEBUG_TERMINAL_WIDTH: f32 = 500.;
const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
const DEBUG_CELL_WIDTH: f32 = 5.;
const DEBUG_LINE_HEIGHT: f32 = 5.;
+// Regex Copied from alacritty's ui_config.rs
+
+lazy_static! {
+ static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap();
+}
+
///Upward flowing events, for changing the title and such
#[derive(Clone, Copy, Debug)]
pub enum Event {
@@ -98,6 +109,8 @@ enum InternalEvent {
ScrollToPoint(Point),
SetSelection(Option<(Selection, Point)>),
UpdateSelection(Vector2F),
+ // Adjusted mouse position, should open
+ Hyperlink(Vector2F, bool),
Copy,
}
@@ -267,7 +280,6 @@ impl TerminalBuilder {
working_directory: Option<PathBuf>,
shell: Option<Shell>,
env: Option<HashMap<String, String>>,
- initial_size: TerminalSize,
blink_settings: Option<TerminalBlink>,
alternate_scroll: &AlternateScroll,
window_id: usize,
@@ -307,7 +319,11 @@ impl TerminalBuilder {
//TODO: Remove with a bounded sender which can be dispatched on &self
let (events_tx, events_rx) = unbounded();
//Set up the terminal...
- let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
+ let mut term = Term::new(
+ &config,
+ &TerminalSize::default(),
+ ZedListener(events_tx.clone()),
+ );
//Start off blinking if we need to
if let Some(TerminalBlink::On) = blink_settings {
@@ -322,7 +338,11 @@ impl TerminalBuilder {
let term = Arc::new(FairMutex::new(term));
//Setup the pty...
- let pty = match tty::new(&pty_config, initial_size.into(), window_id as u64) {
+ let pty = match tty::new(
+ &pty_config,
+ TerminalSize::default().into(),
+ window_id as u64,
+ ) {
Ok(pty) => pty,
Err(error) => {
bail!(TerminalError {
@@ -354,7 +374,6 @@ impl TerminalBuilder {
term,
events: VecDeque::with_capacity(10), //Should never get this high.
last_content: Default::default(),
- cur_size: initial_size,
last_mouse: None,
matches: Vec::new(),
last_synced: Instant::now(),
@@ -365,6 +384,9 @@ impl TerminalBuilder {
foreground_process_info: None,
breadcrumb_text: String::new(),
scroll_px: 0.,
+ last_mouse_position: None,
+ next_link_id: 0,
+ selection_phase: SelectionPhase::Ended,
};
Ok(TerminalBuilder {
@@ -450,6 +472,8 @@ pub struct TerminalContent {
selection: Option<SelectionRange>,
cursor: RenderableCursor,
cursor_char: char,
+ size: TerminalSize,
+ last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
}
impl Default for TerminalContent {
@@ -465,17 +489,27 @@ impl Default for TerminalContent {
point: Point::new(Line(0), Column(0)),
},
cursor_char: Default::default(),
+ size: Default::default(),
+ last_hovered_hyperlink: None,
}
}
}
+#[derive(PartialEq, Eq)]
+pub enum SelectionPhase {
+ Selecting,
+ Ended,
+}
+
pub struct Terminal {
pty_tx: Notifier,
term: Arc<FairMutex<Term<ZedListener>>>,
events: VecDeque<InternalEvent>,
+ /// This is only used for mouse mode cell change detection
last_mouse: Option<(Point, AlacDirection)>,
+ /// This is only used for terminal hyperlink checking
+ last_mouse_position: Option<Vector2F>,
pub matches: Vec<RangeInclusive<Point>>,
- cur_size: TerminalSize,
last_content: TerminalContent,
last_synced: Instant,
sync_task: Option<Task<()>>,
@@ -485,6 +519,8 @@ pub struct Terminal {
shell_fd: u32,
foreground_process_info: Option<LocalProcessInfo>,
scroll_px: f32,
+ next_link_id: usize,
+ selection_phase: SelectionPhase,
}
impl Terminal {
@@ -508,7 +544,7 @@ impl Terminal {
)),
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
AlacTermEvent::TextAreaSizeRequest(format) => {
- self.write_to_pty(format(self.cur_size.into()))
+ self.write_to_pty(format(self.last_content.size.into()))
}
AlacTermEvent::CursorBlinkingChange => {
cx.emit(Event::BlinkChanged);
@@ -577,7 +613,7 @@ impl Terminal {
new_size.height = f32::max(new_size.line_height, new_size.height);
new_size.width = f32::max(new_size.cell_width, new_size.width);
- self.cur_size = new_size.clone();
+ self.last_content.size = new_size.clone();
self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
@@ -606,8 +642,12 @@ impl Terminal {
}
InternalEvent::UpdateSelection(position) => {
if let Some(mut selection) = term.selection.take() {
- let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
- let side = mouse_side(*position, self.cur_size);
+ let point = grid_point(
+ *position,
+ self.last_content.size,
+ term.grid().display_offset(),
+ );
+ let side = mouse_side(*position, self.last_content.size);
selection.update(point, side);
term.selection = Some(selection);
@@ -623,9 +663,52 @@ impl Terminal {
}
}
InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
+ InternalEvent::Hyperlink(position, open) => {
+ let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
+
+ let point = grid_point(
+ *position,
+ self.last_content.size,
+ term.grid().display_offset(),
+ );
+
+ if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) {
+ let url = term.bounds_to_string(*url_match.start(), *url_match.end());
+
+ if *open {
+ open_uri(&url).log_err();
+ } else {
+ self.update_hyperlink(prev_hyperlink, url, url_match);
+ }
+ }
+ }
+ }
+ }
+
+ fn update_hyperlink(
+ &mut self,
+ prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
+ url: String,
+ url_match: RangeInclusive<Point>,
+ ) {
+ if let Some(prev_hyperlink) = prev_hyperlink {
+ if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match {
+ self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2));
+ } else {
+ self.last_content.last_hovered_hyperlink =
+ Some((url, url_match, self.next_link_id()));
+ }
+ } else {
+ self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id()));
}
}
+ fn next_link_id(&mut self) -> usize {
+ let res = self.next_link_id;
+ self.next_link_id = self.next_link_id.wrapping_add(1);
+ res
+ }
+
pub fn last_content(&self) -> &TerminalContent {
&self.last_content
}
@@ -730,11 +813,11 @@ impl Terminal {
self.process_terminal_event(&e, &mut terminal, cx)
}
- self.last_content = Self::make_content(&terminal);
+ self.last_content = Self::make_content(&terminal, &self.last_content);
self.last_synced = Instant::now();
}
- fn make_content(term: &Term<ZedListener>) -> TerminalContent {
+ fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
let content = term.renderable_content();
TerminalContent {
cells: content
@@ -757,6 +840,8 @@ impl Terminal {
selection: content.selection,
cursor: content.cursor,
cursor_char: term.grid()[content.cursor.point].c,
+ size: last_content.size,
+ last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
}
}
@@ -766,7 +851,8 @@ impl Terminal {
}
}
- pub fn focus_out(&self) {
+ pub fn focus_out(&mut self) {
+ self.last_mouse_position = None;
if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
self.write_to_pty("\x1b[O".to_string());
}
@@ -794,22 +880,81 @@ impl Terminal {
}
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
+ let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
+
let position = e.position.sub(origin);
+ self.last_mouse_position = Some(position);
+ if self.mouse_mode(e.shift) {
+ let point = grid_point(
+ position,
+ self.last_content.size,
+ self.last_content.display_offset,
+ );
+ let side = mouse_side(position, self.last_content.size);
+
+ if self.mouse_changed(point, side) {
+ if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
+ self.pty_tx.notify(bytes);
+ }
+ }
+ } else {
+ self.fill_hyperlink(Some(position), prev_hyperlink);
+ }
+ }
+
+ fn fill_hyperlink(
+ &mut self,
+ position: Option<Vector2F>,
+ prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
+ ) {
+ if self.selection_phase == SelectionPhase::Selecting {
+ self.last_content.last_hovered_hyperlink = None;
+ } else if let Some(position) = position {
+ let content_index = content_index_for_mouse(position, &self.last_content);
+ let link = self.last_content.cells[content_index].hyperlink();
+ if link.is_some() {
+ let mut min_index = content_index;
+ loop {
+ if min_index >= 1 && self.last_content.cells[min_index - 1].hyperlink() == link
+ {
+ min_index = min_index - 1;
+ } else {
+ break;
+ }
+ }
- let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
- let side = mouse_side(position, self.cur_size);
+ let mut max_index = content_index;
+ let len = self.last_content.cells.len();
+ loop {
+ if max_index < len - 1
+ && self.last_content.cells[max_index + 1].hyperlink() == link
+ {
+ max_index = max_index + 1;
+ } else {
+ break;
+ }
+ }
- if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
- if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
- self.pty_tx.notify(bytes);
+ if let Some(link) = link {
+ let url = link.uri().to_owned();
+ let url_match = self.last_content.cells[min_index].point
+ ..=self.last_content.cells[max_index].point;
+
+ self.update_hyperlink(prev_hyperlink, url, url_match);
+ };
+ } else {
+ self.events
+ .push_back(InternalEvent::Hyperlink(position, false));
}
}
}
pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
+ self.last_mouse_position = Some(position);
if !self.mouse_mode(e.shift) {
+ self.selection_phase = SelectionPhase::Selecting;
// Alacritty has the same ordering, of first updating the selection
// then scrolling 15ms later
self.events
@@ -822,7 +967,7 @@ impl Terminal {
None => return,
};
- let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
+ let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
self.events
.push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
@@ -834,8 +979,8 @@ impl Terminal {
fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
//TODO: Why do these need to be doubled? Probably the same problem that the IME has
- let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
- let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
+ let top = e.region.origin_y() + (self.last_content.size.line_height * 2.);
+ let bottom = e.region.lower_left().y() - (self.last_content.size.line_height * 2.);
let scroll_delta = if e.position.y() < top {
(top - e.position.y()).powf(1.1)
} else if e.position.y() > bottom {
@@ -848,8 +993,12 @@ impl Terminal {
pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
- let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
- let side = mouse_side(position, self.cur_size);
+ let point = grid_point(
+ position,
+ self.last_content.size,
+ self.last_content.display_offset,
+ );
+ let side = mouse_side(position, self.last_content.size);
if self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
@@ -865,25 +1014,42 @@ impl Terminal {
pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
-
if !self.mouse_mode(e.shift) {
- let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
- let side = mouse_side(position, self.cur_size);
-
- let selection_type = match e.click_count {
- 0 => return, //This is a release
- 1 => Some(SelectionType::Simple),
- 2 => Some(SelectionType::Semantic),
- 3 => Some(SelectionType::Lines),
- _ => None,
- };
+ //Hyperlinks
+ {
+ let mouse_cell_index = content_index_for_mouse(position, &self.last_content);
+ if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
+ open_uri(link.uri()).log_err();
+ } else {
+ self.events
+ .push_back(InternalEvent::Hyperlink(position, true));
+ }
+ }
- let selection =
- selection_type.map(|selection_type| Selection::new(selection_type, point, side));
+ // Selections
+ {
+ let point = grid_point(
+ position,
+ self.last_content.size,
+ self.last_content.display_offset,
+ );
+ let side = mouse_side(position, self.last_content.size);
+
+ let selection_type = match e.click_count {
+ 0 => return, //This is a release
+ 1 => Some(SelectionType::Simple),
+ 2 => Some(SelectionType::Semantic),
+ 3 => Some(SelectionType::Lines),
+ _ => None,
+ };
- if let Some(sel) = selection {
- self.events
- .push_back(InternalEvent::SetSelection(Some((sel, point))));
+ let selection = selection_type
+ .map(|selection_type| Selection::new(selection_type, point, side));
+
+ if let Some(sel) = selection {
+ self.events
+ .push_back(InternalEvent::SetSelection(Some((sel, point))));
+ }
}
}
}
@@ -891,7 +1057,11 @@ impl Terminal {
pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
if self.mouse_mode(e.shift) {
- let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
+ let point = grid_point(
+ position,
+ self.last_content.size,
+ self.last_content.display_offset,
+ );
if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
self.pty_tx.notify(bytes);
@@ -901,6 +1071,7 @@ impl Terminal {
// so let's do that here
self.copy();
}
+ self.selection_phase = SelectionPhase::Ended;
self.last_mouse = None;
}
@@ -910,9 +1081,9 @@ impl Terminal {
if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
if mouse_mode {
- let point = mouse_point(
+ let point = grid_point(
e.position.sub(origin),
- self.cur_size,
+ self.last_content.size,
self.last_content.display_offset,
);
@@ -940,6 +1111,17 @@ impl Terminal {
}
}
+ pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool {
+ let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
+
+ if cmd {
+ self.fill_hyperlink(self.last_mouse_position, prev_hyperlink);
+ true
+ } else {
+ false
+ }
+ }
+
fn determine_scroll_lines(
&mut self,
e: &ScrollWheelRegionEvent,
@@ -955,20 +1137,22 @@ impl Terminal {
}
/* Calculate the appropriate scroll lines */
Some(gpui::TouchPhase::Moved) => {
- let old_offset = (self.scroll_px / self.cur_size.line_height) as i32;
+ let old_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
self.scroll_px += e.delta.y() * scroll_multiplier;
- let new_offset = (self.scroll_px / self.cur_size.line_height) as i32;
+ let new_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
// Whenever we hit the edges, reset our stored scroll to 0
// so we can respond to changes in direction quickly
- self.scroll_px %= self.cur_size.height;
+ self.scroll_px %= self.last_content.size.height;
Some(new_offset - old_offset)
}
/* Fall back to delta / line_height */
- None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32),
+ None => Some(
+ ((e.delta.y() * scroll_multiplier) / self.last_content.size.line_height) as i32,
+ ),
_ => None,
}
}
@@ -1011,30 +1195,36 @@ impl Entity for Terminal {
type Event = Event;
}
+/// Based on alacritty/src/display/hint.rs > regex_match_at
+/// Retrieve the match, if the specified point is inside the content matching the regex.
+fn regex_match_at<T>(term: &Term<T>, point: Point, regex: &RegexSearch) -> Option<Match> {
+ visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
+}
+
+/// Copied from alacritty/src/display/hint.rs:
+/// Iterate over all visible regex matches.
+pub fn visible_regex_match_iter<'a, T>(
+ term: &'a Term<T>,
+ regex: &'a RegexSearch,
+) -> impl Iterator<Item = Match> + 'a {
+ let viewport_start = Line(-(term.grid().display_offset() as i32));
+ let viewport_end = viewport_start + term.bottommost_line();
+ let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
+ let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
+ start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
+ end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
+
+ RegexIter::new(start, end, AlacDirection::Right, term, regex)
+ .skip_while(move |rm| rm.end().line < viewport_start)
+ .take_while(move |rm| rm.start().line <= viewport_end)
+}
+
fn make_selection(range: &RangeInclusive<Point>) -> Selection {
let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
selection.update(*range.end(), AlacDirection::Right);
selection
}
-/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
-/// Iterate over all visible regex matches.
-// fn visible_search_matches<'a, T>(
-// term: &'a Term<T>,
-// regex: &'a RegexSearch,
-// ) -> impl Iterator<Item = Match> + 'a {
-// let viewport_start = Line(-(term.grid().display_offset() as i32));
-// let viewport_end = viewport_start + term.bottommost_line();
-// let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
-// let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
-// start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
-// end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
-
-// RegexIter::new(start, end, AlacDirection::Right, term, regex)
-// .skip_while(move |rm| rm.end().line < viewport_start)
-// .take_while(move |rm| rm.start().line <= viewport_end)
-// }
-
fn all_search_matches<'a, T>(
term: &'a Term<T>,
regex: &'a RegexSearch,
@@ -1044,7 +1234,115 @@ fn all_search_matches<'a, T>(
RegexIter::new(start, end, AlacDirection::Right, term, regex)
}
+fn content_index_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> usize {
+ let col = min(
+ (pos.x() / content.size.cell_width()) as usize,
+ content.size.columns() - 1,
+ ) as usize;
+ let line = min(
+ (pos.y() / content.size.line_height()) as usize,
+ content.size.screen_lines() - 1,
+ ) as usize;
+
+ line * content.size.columns() + col
+}
+
+fn open_uri(uri: &str) -> Result<(), std::io::Error> {
+ let mut command = Command::new("open");
+ command.arg(uri);
+
+ unsafe {
+ command
+ .pre_exec(|| {
+ match libc::fork() {
+ -1 => return Err(io::Error::last_os_error()),
+ 0 => (),
+ _ => libc::_exit(0),
+ }
+
+ if libc::setsid() == -1 {
+ return Err(io::Error::last_os_error());
+ }
+
+ Ok(())
+ })
+ .spawn()?
+ .wait()
+ .map(|_| ())
+ }
+}
+
#[cfg(test)]
mod tests {
+ use gpui::geometry::vector::vec2f;
+ use rand::{thread_rng, Rng};
+
+ use crate::content_index_for_mouse;
+
+ use self::terminal_test_context::TerminalTestContext;
+
pub mod terminal_test_context;
+
+ #[test]
+ fn test_mouse_to_cell() {
+ let mut rng = thread_rng();
+
+ for _ in 0..10 {
+ let viewport_cells = rng.gen_range(5..50);
+ let cell_size = rng.gen_range(5.0..20.0);
+
+ let size = crate::TerminalSize {
+ cell_width: cell_size,
+ line_height: cell_size,
+ height: cell_size * (viewport_cells as f32),
+ width: cell_size * (viewport_cells as f32),
+ };
+
+ let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng);
+
+ for i in 0..(viewport_cells - 1) {
+ let i = i as usize;
+ for j in 0..(viewport_cells - 1) {
+ let j = j as usize;
+ let min_row = i as f32 * cell_size;
+ let max_row = (i + 1) as f32 * cell_size;
+ let min_col = j as f32 * cell_size;
+ let max_col = (j + 1) as f32 * cell_size;
+
+ let mouse_pos = vec2f(
+ rng.gen_range(min_row..max_row),
+ rng.gen_range(min_col..max_col),
+ );
+
+ assert_eq!(
+ content.cells[content_index_for_mouse(mouse_pos, &content)].c,
+ cells[j][i]
+ );
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn test_mouse_to_cell_clamp() {
+ let mut rng = thread_rng();
+
+ let size = crate::TerminalSize {
+ cell_width: 10.,
+ line_height: 10.,
+ height: 100.,
+ width: 100.,
+ };
+
+ let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng);
+
+ assert_eq!(
+ content.cells[content_index_for_mouse(vec2f(-10., -10.), &content)].c,
+ cells[0][0]
+ );
+ assert_eq!(
+ content.cells[content_index_for_mouse(vec2f(1000., 1000.), &content)].c,
+ cells[9][9]
+ );
+ }
}
@@ -11,7 +11,6 @@ use util::truncate_and_trailoff;
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace};
-use crate::TerminalSize;
use project::{LocalWorktree, Project, ProjectPath};
use settings::{AlternateScroll, Settings, WorkingDirectory};
use smallvec::SmallVec;
@@ -87,9 +86,6 @@ impl TerminalContainer {
modal: bool,
cx: &mut ViewContext<Self>,
) -> Self {
- //The exact size here doesn't matter, the terminal will be resized on the first layout
- let size_info = TerminalSize::default();
-
let settings = cx.global::<Settings>();
let shell = settings.terminal_overrides.shell.clone();
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
@@ -111,7 +107,6 @@ impl TerminalContainer {
working_directory.clone(),
shell,
envs,
- size_info,
settings.terminal_overrides.blinking.clone(),
scroll,
cx.window_id(),
@@ -7,15 +7,17 @@ use alacritty_terminal::{
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
use gpui::{
color::Color,
- fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
+ elements::{Empty, Overlay},
+ fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
serde_json::json,
text_layout::{Line, RunStyle},
- Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion,
- PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle,
+ Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext,
+ ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint,
+ TextLayoutCache, WeakModelHandle, WeakViewHandle,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
@@ -42,6 +44,7 @@ pub struct LayoutState {
size: TerminalSize,
mode: TermMode,
display_offset: usize,
+ hyperlink_tooltip: Option<ElementBox>,
}
///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
@@ -180,6 +183,7 @@ impl TerminalElement {
text_layout_cache: &TextLayoutCache,
font_cache: &FontCache,
modal: bool,
+ hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
let mut cells = vec![];
let mut rects = vec![];
@@ -237,7 +241,7 @@ impl TerminalElement {
//Layout current cell text
{
let cell_text = &cell.c.to_string();
- if cell_text != " " {
+ if !is_blank(&cell) {
let cell_style = TerminalElement::cell_style(
&cell,
fg,
@@ -245,6 +249,7 @@ impl TerminalElement {
text_style,
font_cache,
modal,
+ hyperlink,
);
let layout_cell = text_layout_cache.layout_str(
@@ -257,8 +262,8 @@ impl TerminalElement {
Point::new(line_index as i32, cell.point.column.0 as i32),
layout_cell,
))
- }
- };
+ };
+ }
}
if cur_rect.is_some() {
@@ -304,11 +309,12 @@ impl TerminalElement {
text_style: &TextStyle,
font_cache: &FontCache,
modal: bool,
+ hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
) -> RunStyle {
let flags = indexed.cell.flags;
let fg = convert_color(&fg, &style.colors, modal);
- let underline = flags
+ let mut underline = flags
.intersects(Flags::ALL_UNDERLINES)
.then(|| Underline {
color: Some(fg),
@@ -317,6 +323,12 @@ impl TerminalElement {
})
.unwrap_or_default();
+ if indexed.cell.hyperlink().is_some() {
+ if underline.thickness == OrderedFloat(0.) {
+ underline.thickness = OrderedFloat(1.);
+ }
+ }
+
let mut properties = Properties::new();
if indexed
.flags
@@ -332,11 +344,25 @@ impl TerminalElement {
.select_font(text_style.font_family_id, &properties)
.unwrap_or(text_style.font_id);
- RunStyle {
+ let mut result = RunStyle {
color: fg,
font_id,
underline,
+ };
+
+ if let Some((style, range)) = hyperlink {
+ if range.contains(&indexed.point) {
+ if let Some(underline) = style.underline {
+ result.underline = underline;
+ }
+
+ if let Some(color) = style.color {
+ result.color = color;
+ }
+ }
}
+
+ result
}
fn generic_button_handler<E>(
@@ -366,7 +392,7 @@ impl TerminalElement {
) {
let connection = self.terminal;
- let mut region = MouseRegion::new::<Self>(view_id, view_id, visible_bounds);
+ let mut region = MouseRegion::new::<Self>(view_id, 0, visible_bounds);
// Terminal Emulator controlled behavior:
region = region
@@ -428,6 +454,16 @@ impl TerminalElement {
});
}
})
+ .on_move(move |event, cx| {
+ if cx.is_parent_view_focused() {
+ if let Some(conn_handle) = connection.upgrade(cx.app) {
+ conn_handle.update(cx.app, |terminal, cx| {
+ terminal.mouse_move(&event, origin);
+ cx.notify();
+ })
+ }
+ }
+ })
.on_scroll(TerminalElement::generic_button_handler(
connection,
origin,
@@ -481,21 +517,6 @@ impl TerminalElement {
),
)
}
- //Mouse move manages both dragging and motion events
- if mode.intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION) {
- region = region
- //TODO: This does not fire on right-mouse-down-move events.
- .on_move(move |event, cx| {
- if cx.is_parent_view_focused() {
- if let Some(conn_handle) = connection.upgrade(cx.app) {
- conn_handle.update(cx.app, |terminal, cx| {
- terminal.mouse_move(&event, origin);
- cx.notify();
- })
- }
- }
- })
- }
cx.scene.push_mouse_region(region);
}
@@ -547,6 +568,9 @@ impl Element for TerminalElement {
//Setup layout information
let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
+ let link_style = settings.theme.editor.link_definition;
+ let tooltip_style = settings.theme.tooltip.clone();
+
let text_style = TerminalElement::make_text_style(font_cache, settings);
let selection_color = settings.theme.editor.selection.selection;
let match_color = settings.theme.search.match_background;
@@ -569,9 +593,34 @@ impl Element for TerminalElement {
};
let terminal_handle = self.terminal.upgrade(cx).unwrap();
- terminal_handle.update(cx.app, |terminal, cx| {
+ let last_hovered_hyperlink = terminal_handle.update(cx.app, |terminal, cx| {
terminal.set_size(dimensions);
- terminal.try_sync(cx)
+ terminal.try_sync(cx);
+ terminal.last_content.last_hovered_hyperlink.clone()
+ });
+
+ let view_handle = self.view.clone();
+ let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _, id)| {
+ // last_mouse.and_then(|_last_mouse| {
+ view_handle.upgrade(cx).map(|handle| {
+ let mut tooltip = cx.render(&handle, |_, cx| {
+ Overlay::new(
+ Empty::new()
+ .contained()
+ .constrained()
+ .with_width(dimensions.width())
+ .with_height(dimensions.height())
+ .with_tooltip::<TerminalElement, _>(id, uri, None, tooltip_style, cx)
+ .boxed(),
+ )
+ .with_position_mode(gpui::elements::OverlayPositionMode::Local)
+ .boxed()
+ });
+
+ tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
+ tooltip
+ })
+ // })
});
let TerminalContent {
@@ -581,8 +630,9 @@ impl Element for TerminalElement {
cursor_char,
selection,
cursor,
+ last_hovered_hyperlink,
..
- } = &terminal_handle.read(cx).last_content;
+ } = { &terminal_handle.read(cx).last_content };
// searches, highlights to a single range representations
let mut relative_highlighted_ranges = Vec::new();
@@ -602,6 +652,9 @@ impl Element for TerminalElement {
cx.text_layout_cache,
cx.font_cache(),
self.modal,
+ last_hovered_hyperlink
+ .as_ref()
+ .map(|(_, range, _)| (link_style, range)),
);
//Layout cursor. Rectangle is used for IME, so we should lay it out even
@@ -633,10 +686,11 @@ impl Element for TerminalElement {
)
};
+ let focused = self.focused;
TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
move |(cursor_position, block_width)| {
let shape = match cursor.shape {
- AlacCursorShape::Block if !self.focused => CursorShape::Hollow,
+ AlacCursorShape::Block if !focused => CursorShape::Hollow,
AlacCursorShape::Block => CursorShape::Block,
AlacCursorShape::Underline => CursorShape::Underscore,
AlacCursorShape::Beam => CursorShape::Bar,
@@ -669,6 +723,7 @@ impl Element for TerminalElement {
relative_highlighted_ranges,
mode: *mode,
display_offset: *display_offset,
+ hyperlink_tooltip,
},
)
}
@@ -691,7 +746,11 @@ impl Element for TerminalElement {
cx.scene.push_cursor_region(gpui::CursorRegion {
bounds,
- style: gpui::CursorStyle::IBeam,
+ style: if layout.hyperlink_tooltip.is_some() {
+ gpui::CursorStyle::PointingHand
+ } else {
+ gpui::CursorStyle::IBeam
+ },
});
cx.paint_layer(clip_bounds, |cx| {
@@ -743,6 +802,10 @@ impl Element for TerminalElement {
})
}
}
+
+ if let Some(element) = &mut layout.hyperlink_tooltip {
+ element.paint(origin, visible_bounds, cx)
+ }
});
}
@@ -781,6 +844,18 @@ impl Element for TerminalElement {
})
})
.unwrap_or(false)
+ } else if let Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) = event {
+ self.terminal
+ .upgrade(cx.app)
+ .map(|model_handle| {
+ if model_handle.update(cx.app, |term, _| term.refresh_hyperlink(*cmd)) {
+ cx.notify();
+ true
+ } else {
+ false
+ }
+ })
+ .unwrap_or(false)
} else {
false
}
@@ -824,6 +899,29 @@ impl Element for TerminalElement {
}
}
+fn is_blank(cell: &IndexedCell) -> bool {
+ if cell.c != ' ' {
+ return false;
+ }
+
+ if cell.bg != AnsiColor::Named(NamedColor::Background) {
+ return false;
+ }
+
+ if cell.hyperlink().is_some() {
+ return false;
+ }
+
+ if cell
+ .flags
+ .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
+ {
+ return false;
+ }
+
+ return true;
+}
+
fn to_highlighted_range_lines(
range: &RangeInclusive<Point>,
layout: &LayoutState,
@@ -362,7 +362,9 @@ impl View for TerminalView {
}
fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.terminal.read(cx).focus_out();
+ self.terminal.update(cx, |terminal, _| {
+ terminal.focus_out();
+ });
cx.notify();
}
@@ -1,10 +1,17 @@
use std::{path::Path, time::Duration};
+use alacritty_terminal::{
+ index::{Column, Line, Point},
+ term::cell::Cell,
+};
use gpui::{ModelHandle, TestAppContext, ViewHandle};
use project::{Entry, Project, ProjectPath, Worktree};
+use rand::{rngs::ThreadRng, Rng};
use workspace::{AppState, Workspace};
+use crate::{IndexedCell, TerminalContent, TerminalSize};
+
pub struct TerminalTestContext<'a> {
pub cx: &'a mut TestAppContext,
}
@@ -88,6 +95,39 @@ impl<'a> TerminalTestContext<'a> {
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
});
}
+
+ pub fn create_terminal_content(
+ size: TerminalSize,
+ rng: &mut ThreadRng,
+ ) -> (TerminalContent, Vec<Vec<char>>) {
+ let mut ic = Vec::new();
+ let mut cells = Vec::new();
+
+ for row in 0..((size.height() / size.line_height()) as usize) {
+ let mut row_vec = Vec::new();
+ for col in 0..((size.width() / size.cell_width()) as usize) {
+ let cell_char = rng.gen();
+ ic.push(IndexedCell {
+ point: Point::new(Line(row as i32), Column(col)),
+ cell: Cell {
+ c: cell_char,
+ ..Default::default()
+ },
+ });
+ row_vec.push(cell_char)
+ }
+ cells.push(row_vec)
+ }
+
+ (
+ TerminalContent {
+ cells: ic,
+ size,
+ ..Default::default()
+ },
+ cells,
+ )
+ }
}
impl<'a> Drop for TerminalTestContext<'a> {