From 4dbded3f0209f58a042bfa7567602ee2a25a61b6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 14 Sep 2022 15:41:55 -0700 Subject: [PATCH 01/13] Implemented cell for mouse pointer --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 1 + crates/terminal/src/terminal.rs | 143 ++++++++++++++---- .../terminal/src/terminal_container_view.rs | 5 - .../src/tests/terminal_test_context.rs | 40 +++++ 5 files changed, 159 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 634ef452a369b142c983302bcb471c00395e1a19..141a542bd2fea5e6232ad8b569a67a783cb69af1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5391,6 +5391,7 @@ dependencies = [ "ordered-float", "procinfo", "project", + "rand 0.8.5", "settings", "shellexpand", "smallvec", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 7831be1c5af47f1dd9e80bac7f5be5d070e21e51..eb1bc560176bf06a3470936aafba60ab86cff786 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -36,3 +36,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" diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a93dd096705d664df6021d37acf8d6902f6070f1..b9c8021d0099cb8c7ce27efbb67ade18767b4a69 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -270,7 +270,6 @@ impl TerminalBuilder { working_directory: Option, shell: Option, env: Option>, - initial_size: TerminalSize, blink_settings: Option, alternate_scroll: &AlternateScroll, window_id: usize, @@ -310,7 +309,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 { @@ -325,7 +328,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 { @@ -357,7 +364,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(), @@ -453,6 +459,7 @@ pub struct TerminalContent { selection: Option, cursor: RenderableCursor, cursor_char: char, + size: TerminalSize, } impl Default for TerminalContent { @@ -468,6 +475,7 @@ impl Default for TerminalContent { point: Point::new(Line(0), Column(0)), }, cursor_char: Default::default(), + size: Default::default(), } } } @@ -478,7 +486,6 @@ pub struct Terminal { events: VecDeque, last_mouse: Option<(Point, AlacDirection)>, pub matches: Vec>, - cur_size: TerminalSize, last_content: TerminalContent, last_synced: Instant, sync_task: Option>, @@ -511,7 +518,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); @@ -580,7 +587,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(); @@ -609,8 +616,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 = mouse_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); @@ -733,11 +744,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.size); self.last_synced = Instant::now(); } - fn make_content(term: &Term) -> TerminalContent { + fn make_content(term: &Term, last_size: TerminalSize) -> TerminalContent { let content = term.renderable_content(); TerminalContent { cells: content @@ -760,6 +771,7 @@ impl Terminal { selection: content.selection, cursor: content.cursor, cursor_char: term.grid()[content.cursor.point].c, + size: last_size, } } @@ -799,8 +811,12 @@ impl Terminal { pub fn mouse_move(&mut self, e: &MouseMovedEvent, 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 = mouse_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) && self.mouse_mode(e.shift) { if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) { @@ -825,7 +841,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))); @@ -837,8 +853,8 @@ impl Terminal { fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option { //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 { @@ -851,8 +867,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 = mouse_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) { @@ -870,8 +890,12 @@ impl Terminal { 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 point = mouse_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 @@ -894,7 +918,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 = mouse_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); @@ -915,7 +943,7 @@ impl Terminal { if mouse_mode { let point = mouse_point( e.position.sub(origin), - self.cur_size, + self.last_content.size, self.last_content.display_offset, ); @@ -954,20 +982,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, } } @@ -1043,7 +1073,68 @@ fn all_search_matches<'a, T>( RegexIter::new(start, end, AlacDirection::Right, term, regex) } +fn cell_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> &'a IndexedCell { + fn pos_to_viewport(pos: Vector2F, size: TerminalSize) -> Point { + Point { + line: Line((pos.x() / size.cell_width()) as i32), + column: Column((pos.y() / size.line_height()) as usize), + } + } + + fn cell_for_pos<'a>(point: Point, content: &'a TerminalContent) -> &'a IndexedCell { + dbg!(point.line.0, content.size.columns(), point.column.0); + debug_assert!(point.line.0.is_positive() || point.line.0 == 0); + &content.cells[(point.line.0 as usize * content.size.columns() + point.column.0)] + } + + cell_for_pos(pos_to_viewport(pos, content.size), &content) +} + #[cfg(test)] mod tests { + use gpui::geometry::vector::vec2f; + use rand::{thread_rng, Rng}; + + use crate::cell_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 { + let i = i as usize; + for j in 0..viewport_cells { + 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!(cell_for_mouse(mouse_pos, &content).c, cells[i][j]); + } + } + } + } } diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index 1aebd1f5e7a06200594df31bd4bc2dd6fc42b356..e0fe6ef6cbf275d768970da9d926b1adda7b509f 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -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 { - //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::(); 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(), diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index bee78e3ce0b3feeef432215ee6e02d5213668223..d2b5e2aaa68151119b300cc7eb67d84eeaa4b1fa 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -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, } @@ -86,6 +93,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>) { + 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> { From ac390745a73fae22f20c012b540c4c7e3ddf2758 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 16 Sep 2022 19:35:18 -0700 Subject: [PATCH 02/13] WIP hyperlinks --- crates/terminal/src/mappings/mouse.rs | 6 +- crates/terminal/src/terminal.rs | 117 ++++++++++++++++---------- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/crates/terminal/src/mappings/mouse.rs b/crates/terminal/src/mappings/mouse.rs index c90cbb5cd3887f9ee650817bfe0816e9a611f8fa..e9d163416558075c66128056fdf27265ef665940 100644 --- a/crates/terminal/src/mappings/mouse.rs +++ b/crates/terminal/src/mappings/mouse.rs @@ -201,7 +201,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; @@ -294,7 +294,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() { @@ -316,7 +316,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( diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b9c8021d0099cb8c7ce27efbb67ade18767b4a69..5049008c37ba9dc65ec0c34344dbcde4a96e3358 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -30,7 +30,7 @@ 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 modal::deploy_modal; @@ -616,7 +616,7 @@ impl Terminal { } InternalEvent::UpdateSelection(position) => { if let Some(mut selection) = term.selection.take() { - let point = mouse_point( + let point = grid_point( *position, self.last_content.size, term.grid().display_offset(), @@ -809,18 +809,27 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - let position = e.position.sub(origin); + if self.mouse_mode(e.shift) { + let position = e.position.sub(origin); - let point = mouse_point( - position, - self.last_content.size, - self.last_content.display_offset, - ); - let side = mouse_side(position, self.last_content.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_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 self.mouse_changed(point, side) { + if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) { + self.pty_tx.notify(bytes); + } + } + } else { + if let Some(link) = cell_for_mouse(e.position, &self.last_content) + .cell + .hyperlink() + { + link.uri() } } } @@ -867,7 +876,7 @@ impl Terminal { pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); - let point = mouse_point( + let point = grid_point( position, self.last_content.size, self.last_content.display_offset, @@ -890,7 +899,7 @@ impl Terminal { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { - let point = mouse_point( + let point = grid_point( position, self.last_content.size, self.last_content.display_offset, @@ -918,7 +927,7 @@ 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( + let point = grid_point( position, self.last_content.size, self.last_content.display_offset, @@ -941,7 +950,7 @@ 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.last_content.size, self.last_content.display_offset, @@ -1046,24 +1055,6 @@ fn make_selection(range: &RangeInclusive) -> Selection { 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, -// regex: &'a RegexSearch, -// ) -> impl Iterator + '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, regex: &'a RegexSearch, @@ -1074,20 +1065,56 @@ fn all_search_matches<'a, T>( } fn cell_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> &'a IndexedCell { - fn pos_to_viewport(pos: Vector2F, size: TerminalSize) -> Point { - Point { - line: Line((pos.x() / size.cell_width()) as i32), - column: Column((pos.y() / size.line_height()) as usize), + let point = Point { + line: Line((pos.x() / content.size.cell_width()) as i32), + column: Column((pos.y() / content.size.line_height()) as usize), + }; + + debug_assert!(point.line.0.is_positive() || point.line.0 == 0); + &content.cells[(point.line.0 as usize * content.size.columns() + point.column.0)] +} + +fn open_uri(uri: String) { + // MacOS command is 'open' + pub fn spawn_daemon( + program: &str, + args: I, + master_fd: RawFd, + shell_pid: u32, + ) -> io::Result<()> + where + I: IntoIterator + Copy, + S: AsRef, + { + let mut command = Command::new(program); + command + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + if let Ok(cwd) = foreground_process_path(master_fd, shell_pid) { + command.current_dir(cwd); } - } + unsafe { + command + .pre_exec(|| { + match libc::fork() { + -1 => return Err(io::Error::last_os_error()), + 0 => (), + _ => libc::_exit(0), + } - fn cell_for_pos<'a>(point: Point, content: &'a TerminalContent) -> &'a IndexedCell { - dbg!(point.line.0, content.size.columns(), point.column.0); - debug_assert!(point.line.0.is_positive() || point.line.0 == 0); - &content.cells[(point.line.0 as usize * content.size.columns() + point.column.0)] - } + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } - cell_for_pos(pos_to_viewport(pos, content.size), &content) + Ok(()) + }) + .spawn()? + .wait() + .map(|_| ()) + } + } } #[cfg(test)] From 1993a870e183e8f9b3be08d7223e8bb8d5a7fb46 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sun, 18 Sep 2022 23:33:06 -0700 Subject: [PATCH 03/13] Hyperlink clicking is working --- crates/terminal/src/terminal.rs | 165 +++++++++++++++++--------------- 1 file changed, 88 insertions(+), 77 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 5049008c37ba9dc65ec0c34344dbcde4a96e3358..46fcb403110e71b5f49591ff8c7809478330140f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -14,7 +14,7 @@ use alacritty_terminal::{ selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, term::{ - cell::Cell, + cell::{Cell, Hyperlink}, color::Rgb, search::{Match, RegexIter, RegexSearch}, RenderableCursor, TermMode, @@ -36,13 +36,17 @@ use modal::deploy_modal; 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}, }; @@ -374,6 +378,7 @@ impl TerminalBuilder { foreground_process_info: None, breadcrumb_text: String::new(), scroll_px: 0., + last_hovered_hyperlink: None, }; Ok(TerminalBuilder { @@ -488,6 +493,7 @@ pub struct Terminal { pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, + last_hovered_hyperlink: Option, sync_task: Option>, selection_head: Option, breadcrumb_text: String, @@ -809,9 +815,10 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - if self.mouse_mode(e.shift) { - let position = e.position.sub(origin); + self.last_hovered_hyperlink = None; + let position = e.position.sub(origin); + if self.mouse_mode(e.shift) { let point = grid_point( position, self.last_content.size, @@ -824,13 +831,10 @@ impl Terminal { self.pty_tx.notify(bytes); } } - } else { - if let Some(link) = cell_for_mouse(e.position, &self.last_content) + } else if e.cmd { + self.last_hovered_hyperlink = cell_for_mouse(e.position, &self.last_content) .cell - .hyperlink() - { - link.uri() - } + .hyperlink(); } } @@ -897,29 +901,34 @@ 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 = 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 e.cmd { + if let Some(link) = cell_for_mouse(position, &self.last_content).hyperlink() { + open_uri(link.uri()).log_err(); + } + } else { + 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, + }; - let selection = - selection_type.map(|selection_type| Selection::new(selection_type, point, side)); + 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)))); + if let Some(sel) = selection { + self.events + .push_back(InternalEvent::SetSelection(Some((sel, point)))); + } } } } @@ -1065,55 +1074,40 @@ fn all_search_matches<'a, T>( } fn cell_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> &'a IndexedCell { - let point = Point { - line: Line((pos.x() / content.size.cell_width()) as i32), - column: Column((pos.y() / content.size.line_height()) as usize), - }; - - debug_assert!(point.line.0.is_positive() || point.line.0 == 0); - &content.cells[(point.line.0 as usize * content.size.columns() + point.column.0)] + 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; + + &content.cells[(line * content.size.columns() + col)] } -fn open_uri(uri: String) { - // MacOS command is 'open' - pub fn spawn_daemon( - program: &str, - args: I, - master_fd: RawFd, - shell_pid: u32, - ) -> io::Result<()> - where - I: IntoIterator + Copy, - S: AsRef, - { - let mut command = Command::new(program); +fn open_uri(uri: &str) -> Result<(), std::io::Error> { + let mut command = Command::new("open"); + command.arg(uri); + + unsafe { command - .args(args) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - if let Ok(cwd) = foreground_process_path(master_fd, shell_pid) { - command.current_dir(cwd); - } - unsafe { - command - .pre_exec(|| { - match libc::fork() { - -1 => return Err(io::Error::last_os_error()), - 0 => (), - _ => libc::_exit(0), - } + .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()); - } + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } - Ok(()) - }) - .spawn()? - .wait() - .map(|_| ()) - } + Ok(()) + }) + .spawn()? + .wait() + .map(|_| ()) } } @@ -1145,9 +1139,9 @@ mod tests { let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng); - for i in 0..viewport_cells { + for i in 0..(viewport_cells - 1) { let i = i as usize; - for j in 0..viewport_cells { + 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; @@ -1159,9 +1153,26 @@ mod tests { rng.gen_range(min_col..max_col), ); - assert_eq!(cell_for_mouse(mouse_pos, &content).c, cells[i][j]); + assert_eq!(cell_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!(cell_for_mouse(vec2f(-10., -10.), &content).c, cells[0][0]); + assert_eq!(cell_for_mouse(vec2f(1000., 1000.), &content).c, cells[9][9]); + } } From 9f81f39f510d4f77a7417214e891f9f0489d88cb Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 19 Sep 2022 09:07:41 -0700 Subject: [PATCH 04/13] WIP Hyperlinks --- crates/terminal/src/terminal.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 46fcb403110e71b5f49591ff8c7809478330140f..9da2831740578f7f5320b70861a9d8f6cf0cfb47 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -43,7 +43,7 @@ use std::{ collections::{HashMap, VecDeque}, fmt::Display, io, - ops::{Deref, RangeInclusive, Sub}, + ops::{Deref, Range, RangeInclusive, Sub}, os::unix::{prelude::AsRawFd, process::CommandExt}, path::PathBuf, process::Command, @@ -493,7 +493,7 @@ pub struct Terminal { pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, - last_hovered_hyperlink: Option, + last_hovered_hyperlink: Option<(Hyperlink, Range)>, sync_task: Option>, selection_head: Option, breadcrumb_text: String, @@ -832,7 +832,7 @@ impl Terminal { } } } else if e.cmd { - self.last_hovered_hyperlink = cell_for_mouse(e.position, &self.last_content) + let hyperlink = cell_for_mouse(e.position, &self.last_content) .cell .hyperlink(); } From b8f362fd843c50c2a76eb21595a8566e85d234f6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 19 Sep 2022 13:41:35 -0700 Subject: [PATCH 05/13] WIP hyperlink detection --- crates/terminal/src/terminal.rs | 81 +++++++++++++++++++++---- crates/terminal/src/terminal_element.rs | 25 +++----- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9da2831740578f7f5320b70861a9d8f6cf0cfb47..e67f3bdbb0e73bb040aa4f36d4f2f70934ab4b69 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -83,6 +83,11 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; +/// Copied from alacritty's ui_config.rs +const URL_REGEX: &str = + "(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ + [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+"; + ///Upward flowing events, for changing the title and such #[derive(Clone, Copy, Debug)] pub enum Event { @@ -105,6 +110,7 @@ enum InternalEvent { ScrollToPoint(Point), SetSelection(Option<(Selection, Point)>), UpdateSelection(Vector2F), + HyperlinkHover(Vector2F), Copy, } @@ -643,6 +649,16 @@ impl Terminal { } } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), + InternalEvent::HyperlinkHover(position) => { + let point = grid_point( + *position, + self.last_content.size, + term.grid().display_offset(), + ); + let side = mouse_side(*position, self.last_content.size); + + println!("Hyperlink hover") + } } } @@ -817,7 +833,6 @@ impl Terminal { pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { self.last_hovered_hyperlink = None; let position = e.position.sub(origin); - if self.mouse_mode(e.shift) { let point = grid_point( position, @@ -832,9 +847,39 @@ impl Terminal { } } } else if e.cmd { - let hyperlink = cell_for_mouse(e.position, &self.last_content) - .cell - .hyperlink(); + 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 self.last_content.cells[min_index - 1].hyperlink() == link { + min_index = min_index - 1; + } else { + break; + } + } + + let mut max_index = content_index; + loop { + if self.last_content.cells[max_index + 1].hyperlink() == link { + max_index = max_index + 1; + } else { + break; + } + } + + self.last_hovered_hyperlink = link.map(|link| { + ( + link, + self.last_content.cells[min_index].point + ..self.last_content.cells[max_index].point, + ) + }); + } else { + self.events + .push_back(InternalEvent::HyperlinkHover(position)); + } } } @@ -903,7 +948,12 @@ impl Terminal { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { if e.cmd { - if let Some(link) = cell_for_mouse(position, &self.last_content).hyperlink() { + if let Some(link) = self.last_content.cells + [content_index_for_mouse(position, &self.last_content)] + .hyperlink() + { + dbg!(&link); + dbg!(&self.last_hovered_hyperlink); open_uri(link.uri()).log_err(); } } else { @@ -1073,7 +1123,7 @@ fn all_search_matches<'a, T>( RegexIter::new(start, end, AlacDirection::Right, term, regex) } -fn cell_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> &'a IndexedCell { +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, @@ -1083,7 +1133,7 @@ fn cell_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> &'a Indexe content.size.screen_lines() - 1, ) as usize; - &content.cells[(line * content.size.columns() + col)] + line * content.size.columns() + col } fn open_uri(uri: &str) -> Result<(), std::io::Error> { @@ -1116,7 +1166,7 @@ mod tests { use gpui::geometry::vector::vec2f; use rand::{thread_rng, Rng}; - use crate::cell_for_mouse; + use crate::content_index_for_mouse; use self::terminal_test_context::TerminalTestContext; @@ -1153,7 +1203,10 @@ mod tests { rng.gen_range(min_col..max_col), ); - assert_eq!(cell_for_mouse(mouse_pos, &content).c, cells[j][i]); + assert_eq!( + content.cells[content_index_for_mouse(mouse_pos, &content)].c, + cells[j][i] + ); } } } @@ -1172,7 +1225,13 @@ mod tests { let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng); - assert_eq!(cell_for_mouse(vec2f(-10., -10.), &content).c, cells[0][0]); - assert_eq!(cell_for_mouse(vec2f(1000., 1000.), &content).c, cells[9][9]); + 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] + ); } } diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 51ebf63e5e41dd3caccd397119bc881a5710613c..52949ef213f4592272872d51064a84176261b952 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -427,6 +427,16 @@ impl TerminalElement { position: e.position, }); } + }) + .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(); + }) + } + } }); // Mouse mode handlers: @@ -474,21 +484,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); } From ba32dcbb88d401c59060856714c82159b4625f08 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 19 Sep 2022 17:19:03 -0700 Subject: [PATCH 06/13] Reworking hyperlink events --- crates/terminal/src/terminal.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e67f3bdbb0e73bb040aa4f36d4f2f70934ab4b69..d1e417c9b80c85ea322e0f57f8dc85d17b1f8658 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -110,7 +110,7 @@ enum InternalEvent { ScrollToPoint(Point), SetSelection(Option<(Selection, Point)>), UpdateSelection(Vector2F), - HyperlinkHover(Vector2F), + Hyperlink(Vector2F, bool), Copy, } @@ -649,7 +649,7 @@ impl Terminal { } } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), - InternalEvent::HyperlinkHover(position) => { + InternalEvent::Hyperlink(position, _hover) => { let point = grid_point( *position, self.last_content.size, @@ -657,7 +657,7 @@ impl Terminal { ); let side = mouse_side(*position, self.last_content.size); - println!("Hyperlink hover") + println!("Hyperlink hover | click ") } } } @@ -878,7 +878,7 @@ impl Terminal { }); } else { self.events - .push_back(InternalEvent::HyperlinkHover(position)); + .push_back(InternalEvent::Hyperlink(position, false)); } } } @@ -955,6 +955,9 @@ impl Terminal { dbg!(&link); dbg!(&self.last_hovered_hyperlink); open_uri(link.uri()).log_err(); + } else { + self.events + .push_back(InternalEvent::Hyperlink(position, true)); } } else { let point = grid_point( From b3202c382dd9290956e912674cd097462abb917e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 19 Sep 2022 17:21:24 -0700 Subject: [PATCH 07/13] WI{ --- crates/terminal/src/terminal.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d1e417c9b80c85ea322e0f57f8dc85d17b1f8658..72aa28cb808c17e0ff8b9118e52b10a27d716267 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -110,6 +110,7 @@ enum InternalEvent { ScrollToPoint(Point), SetSelection(Option<(Selection, Point)>), UpdateSelection(Vector2F), + // Adjusted mouse position, should open Hyperlink(Vector2F, bool), Copy, } @@ -649,7 +650,7 @@ impl Terminal { } } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), - InternalEvent::Hyperlink(position, _hover) => { + InternalEvent::Hyperlink(position, open) => { let point = grid_point( *position, self.last_content.size, From f706cbe1432f65033b7aad41421974863ff72be5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 20 Sep 2022 11:20:57 -0700 Subject: [PATCH 08/13] WIP hyperlink searching --- crates/terminal/src/terminal.rs | 53 ++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 72aa28cb808c17e0ff8b9118e52b10a27d716267..bf5c98d03cb1a64d9e482a4d73cd612c678f954f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -14,7 +14,7 @@ use alacritty_terminal::{ selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, term::{ - cell::{Cell, Hyperlink}, + cell::Cell, color::Rgb, search::{Match, RegexIter, RegexSearch}, RenderableCursor, TermMode, @@ -43,7 +43,7 @@ use std::{ collections::{HashMap, VecDeque}, fmt::Display, io, - ops::{Deref, Range, RangeInclusive, Sub}, + ops::{Deref, RangeInclusive, Sub}, os::unix::{prelude::AsRawFd, process::CommandExt}, path::PathBuf, process::Command, @@ -77,16 +77,14 @@ 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.; /// Copied from alacritty's ui_config.rs -const URL_REGEX: &str = - "(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ - [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+"; +static 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)] @@ -500,7 +498,7 @@ pub struct Terminal { pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, - last_hovered_hyperlink: Option<(Hyperlink, Range)>, + last_hovered_hyperlink: Option<(String, RangeInclusive)>, sync_task: Option>, selection_head: Option, breadcrumb_text: String, @@ -651,14 +649,23 @@ impl Terminal { } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), InternalEvent::Hyperlink(position, open) => { + self.last_hovered_hyperlink = None; + let point = grid_point( *position, self.last_content.size, term.grid().display_offset(), ); - let side = mouse_side(*position, self.last_content.size); - println!("Hyperlink hover | click ") + 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.last_hovered_hyperlink = Some((url, url_match)); + } + } } } } @@ -872,9 +879,9 @@ impl Terminal { self.last_hovered_hyperlink = link.map(|link| { ( - link, + link.uri().to_owned(), self.last_content.cells[min_index].point - ..self.last_content.cells[max_index].point, + ..=self.last_content.cells[max_index].point, ) }); } else { @@ -1112,6 +1119,30 @@ 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(term: &Term, point: Point, regex: &RegexSearch) -> Option { + 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, + regex: &'a RegexSearch, +) -> impl Iterator + '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) -> Selection { let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left); selection.update(*range.end(), AlacDirection::Right); From 5cd56584b45a534b64ef83776b72d294305c32ee Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 22 Sep 2022 22:40:22 -0700 Subject: [PATCH 09/13] Completed terminal hyperlink clicking functionality. Just need to display it now --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 2 ++ crates/terminal/src/terminal.rs | 17 +++++++++-------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 141a542bd2fea5e6232ad8b569a67a783cb69af1..34a1324a41796c81cc3b1464fe3688e16da38c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5386,6 +5386,7 @@ dependencies = [ "futures", "gpui", "itertools", + "lazy_static", "libc", "mio-extras", "ordered-float", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index eb1bc560176bf06a3470936aafba60ab86cff786..da59979145955131c50f4614d9444f8c00cf3934 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -29,6 +29,8 @@ shellexpand = "2.1.0" libc = "0.2" anyhow = "1" thiserror = "1.0" +lazy_static = "1.4.0" + [dev-dependencies] diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index bf5c98d03cb1a64d9e482a4d73cd612c678f954f..ebbfd4fec3c3a25d0f128053720cecbefa198fa2 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -64,6 +64,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) { @@ -83,8 +84,11 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; -/// Copied from alacritty's ui_config.rs -static 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(); +// 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)] @@ -659,6 +663,7 @@ impl Terminal { if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) { let url = term.bounds_to_string(*url_match.start(), *url_match.end()); + dbg!(&url, &url_match, open); if *open { open_uri(&url).log_err(); @@ -956,12 +961,8 @@ impl Terminal { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { if e.cmd { - if let Some(link) = self.last_content.cells - [content_index_for_mouse(position, &self.last_content)] - .hyperlink() - { - dbg!(&link); - dbg!(&self.last_hovered_hyperlink); + 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 From 0584b2f5f05f24914e7435c72b4db508ef7f0803 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 22 Sep 2022 23:04:49 -0700 Subject: [PATCH 10/13] added the fields for drawing the hyperlinks --- crates/terminal/src/terminal.rs | 20 ++++++------- crates/terminal/src/terminal_element.rs | 39 ++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index ebbfd4fec3c3a25d0f128053720cecbefa198fa2..c3b3c06e338da1f3da698ff9f7ec532bb7391920 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -387,7 +387,6 @@ impl TerminalBuilder { foreground_process_info: None, breadcrumb_text: String::new(), scroll_px: 0., - last_hovered_hyperlink: None, }; Ok(TerminalBuilder { @@ -474,6 +473,7 @@ pub struct TerminalContent { cursor: RenderableCursor, cursor_char: char, size: TerminalSize, + last_hovered_hyperlink: Option<(String, RangeInclusive)>, } impl Default for TerminalContent { @@ -490,6 +490,7 @@ impl Default for TerminalContent { }, cursor_char: Default::default(), size: Default::default(), + last_hovered_hyperlink: None, } } } @@ -502,7 +503,6 @@ pub struct Terminal { pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, - last_hovered_hyperlink: Option<(String, RangeInclusive)>, sync_task: Option>, selection_head: Option, breadcrumb_text: String, @@ -653,7 +653,7 @@ impl Terminal { } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), InternalEvent::Hyperlink(position, open) => { - self.last_hovered_hyperlink = None; + self.last_content.last_hovered_hyperlink = None; let point = grid_point( *position, @@ -663,12 +663,11 @@ impl Terminal { if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) { let url = term.bounds_to_string(*url_match.start(), *url_match.end()); - dbg!(&url, &url_match, open); if *open { open_uri(&url).log_err(); } else { - self.last_hovered_hyperlink = Some((url, url_match)); + self.last_content.last_hovered_hyperlink = Some((url, url_match)); } } } @@ -779,11 +778,11 @@ impl Terminal { self.process_terminal_event(&e, &mut terminal, cx) } - self.last_content = Self::make_content(&terminal, self.last_content.size); + self.last_content = Self::make_content(&terminal, &self.last_content); self.last_synced = Instant::now(); } - fn make_content(term: &Term, last_size: TerminalSize) -> TerminalContent { + fn make_content(term: &Term, last_content: &TerminalContent) -> TerminalContent { let content = term.renderable_content(); TerminalContent { cells: content @@ -806,7 +805,8 @@ impl Terminal { selection: content.selection, cursor: content.cursor, cursor_char: term.grid()[content.cursor.point].c, - size: last_size, + size: last_content.size, + last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(), } } @@ -844,7 +844,7 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - self.last_hovered_hyperlink = None; + self.last_content.last_hovered_hyperlink = None; let position = e.position.sub(origin); if self.mouse_mode(e.shift) { let point = grid_point( @@ -882,7 +882,7 @@ impl Terminal { } } - self.last_hovered_hyperlink = link.map(|link| { + self.last_content.last_hovered_hyperlink = link.map(|link| { ( link.uri().to_owned(), self.last_content.cells[min_index].point diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 52949ef213f4592272872d51064a84176261b952..88f48fc4efd0f9f299567c7acec6ec6c0de398b3 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -237,7 +237,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, @@ -257,8 +257,8 @@ impl TerminalElement { Point::new(line_index as i32, cell.point.column.0 as i32), layout_cell, )) - } - }; + }; + } } if cur_rect.is_some() { @@ -308,7 +308,7 @@ impl TerminalElement { 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 +317,13 @@ impl TerminalElement { }) .unwrap_or_default(); + if indexed.cell.hyperlink().is_some() { + underline.squiggly = true; + if underline.thickness == OrderedFloat(0.) { + underline.thickness = OrderedFloat(1.); + } + } + let mut properties = Properties::new(); if indexed .flags @@ -569,6 +576,7 @@ impl Element for TerminalElement { cursor_char, selection, cursor, + last_hovered_hyperlink, .. } = &terminal_handle.read(cx).last_content; @@ -824,6 +832,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, layout: &LayoutState, From d2d49633f142aa67fd7ce49629216268f6d50278 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 24 Sep 2022 08:32:06 -0700 Subject: [PATCH 11/13] WIP, almost done with tooltips --- crates/gpui/src/elements/tooltip.rs | 8 +- crates/terminal/src/terminal.rs | 27 +++++- crates/terminal/src/terminal_element.rs | 117 +++++++++++++++++++++--- crates/terminal/src/terminal_view.rs | 4 +- 4 files changed, 138 insertions(+), 18 deletions(-) diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 55ab7e44d25e0fb8edfdcdb210a6630ce62bbad6..c86230a5e158f30c5cef1716b67113a664b3a4df 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -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>, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 6dab9e60afeb1d6bb1828c950cac1d417fb9aceb..90671dab1ae07337f05691c2c63139066171b60f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -384,6 +384,7 @@ impl TerminalBuilder { foreground_process_info: None, breadcrumb_text: String::new(), scroll_px: 0., + last_mouse_position: None, }; Ok(TerminalBuilder { @@ -496,7 +497,10 @@ pub struct Terminal { pty_tx: Notifier, term: Arc>>, events: VecDeque, + /// 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, pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, @@ -813,7 +817,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()); } @@ -843,6 +848,7 @@ impl Terminal { pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { self.last_content.last_hovered_hyperlink = None; let position = e.position.sub(origin); + self.last_mouse_position = Some(position); if self.mouse_mode(e.shift) { let point = grid_point( position, @@ -857,9 +863,14 @@ impl Terminal { } } } else if e.cmd { + self.fill_hyperlink(Some(position)); + } + } + + fn fill_hyperlink(&mut self, position: Option) { + 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 { @@ -895,6 +906,7 @@ impl Terminal { 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) { // Alacritty has the same ordering, of first updating the selection @@ -1048,6 +1060,17 @@ impl Terminal { } } + pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool { + self.last_content.last_hovered_hyperlink = None; + + if cmd { + self.fill_hyperlink(self.last_mouse_position); + true + } else { + false + } + } + fn determine_scroll_lines( &mut self, e: &ScrollWheelRegionEvent, diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index b66db8b440af9c2fe971c2637f5adcae2911ac4a..9c47e33da3f61372105ee2e7fc43b4c8189110b4 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -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::{Overlay, Tooltip}, + 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, + Axis, 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, } ///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)>, ) -> (Vec, Vec) { let mut cells = vec![]; let mut rects = vec![]; @@ -245,6 +249,7 @@ impl TerminalElement { text_style, font_cache, modal, + hyperlink, ); let layout_cell = text_layout_cache.layout_str( @@ -304,6 +309,7 @@ impl TerminalElement { text_style: &TextStyle, font_cache: &FontCache, modal: bool, + hyperlink: Option<(HighlightStyle, &RangeInclusive)>, ) -> RunStyle { let flags = indexed.cell.flags; let fg = convert_color(&fg, &style.colors, modal); @@ -339,11 +345,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( @@ -373,7 +393,7 @@ impl TerminalElement { ) { let connection = self.terminal; - let mut region = MouseRegion::new::(view_id, view_id, visible_bounds); + let mut region = MouseRegion::new::(view_id, 0, visible_bounds); // Terminal Emulator controlled behavior: region = region @@ -549,6 +569,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; @@ -571,9 +594,51 @@ impl Element for TerminalElement { }; let terminal_handle = self.terminal.upgrade(cx).unwrap(); - terminal_handle.update(cx.app, |terminal, cx| { - terminal.set_size(dimensions); - terminal.try_sync(cx) + let (last_hovered_hyperlink, last_mouse) = + terminal_handle.update(cx.app, |terminal, cx| { + terminal.set_size(dimensions); + terminal.try_sync(cx); + ( + terminal.last_content.last_hovered_hyperlink.clone(), + terminal.last_mouse_position, + ) + }); + + let view_handle = self.view.clone(); + let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _)| { + last_mouse.and_then(|last_mouse| { + view_handle.upgrade(cx).map(|handle| { + let mut tooltip = cx.render(&handle, |_, cx| { + // TODO: Use the correct dynamic line height + // let mut collapsed_tooltip = Tooltip::render_tooltip( + // uri.clone(), + // tooltip_style.clone(), + // None, + // false, + // ) + // .boxed(); + + Overlay::new( + Tooltip::render_tooltip(uri, tooltip_style, None, false) + .constrained() + .with_height(text_style.line_height(cx.font_cache())) + // .dynamically(move |constraint, cx| { + // SizeConstraint::strict_along( + // Axis::Vertical, + // collapsed_tooltip.layout(constraint, cx).y(), + // ) + // }) + .boxed(), + ) + .with_fit_mode(gpui::elements::OverlayFitMode::SwitchAnchor) + .with_anchor_position(last_mouse) + .boxed() + }); + + tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx); + tooltip + }) + }) }); let TerminalContent { @@ -585,7 +650,7 @@ impl Element for TerminalElement { 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(); @@ -605,6 +670,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 @@ -636,10 +704,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, @@ -672,6 +741,7 @@ impl Element for TerminalElement { relative_highlighted_ranges, mode: *mode, display_offset: *display_offset, + hyperlink_tooltip, }, ) } @@ -694,7 +764,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| { @@ -746,6 +820,15 @@ impl Element for TerminalElement { }) } } + + if let Some(element) = &mut layout.hyperlink_tooltip { + element.paint( + visible_bounds.lower_left() + - vec2f(-layout.size.cell_width, layout.size.line_height), + visible_bounds, + cx, + ) + } }); } @@ -784,6 +867,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 } diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index fc8bf20ca7e994e41641c027f89fdf28f54fbade..33d573a76a2755e60548c42d389d1099f19d5694 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -362,7 +362,9 @@ impl View for TerminalView { } fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.terminal.read(cx).focus_out(); + self.terminal.update(cx, |terminal, _| { + terminal.focus_out(); + }); cx.notify(); } From 4bc0afdafa1e0136d2f6e2c09f580a84995afb54 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 16:33:29 -0700 Subject: [PATCH 12/13] Finished terminal hyperlinks for now --- crates/gpui/src/elements/overlay.rs | 29 +++++++- crates/terminal/src/terminal.rs | 89 +++++++++++++++++++------ crates/terminal/src/terminal_element.rs | 79 ++++++++-------------- 3 files changed, 124 insertions(+), 73 deletions(-) diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 20b6c75c8fd6906713a5cf2e2c0afc101eefe992..d47a39e958b8cc18ac12e5b32fa52c602ad0d714 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -14,6 +14,7 @@ pub struct Overlay { anchor_position: Option, 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 => { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 90671dab1ae07337f05691c2c63139066171b60f..56617001cfc264a8bf20f7c50fbae239cac1b800 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -385,6 +385,8 @@ impl TerminalBuilder { breadcrumb_text: String::new(), scroll_px: 0., last_mouse_position: None, + next_link_id: 0, + selection_phase: SelectionPhase::Ended, }; Ok(TerminalBuilder { @@ -471,7 +473,7 @@ pub struct TerminalContent { cursor: RenderableCursor, cursor_char: char, size: TerminalSize, - last_hovered_hyperlink: Option<(String, RangeInclusive)>, + last_hovered_hyperlink: Option<(String, RangeInclusive, usize)>, } impl Default for TerminalContent { @@ -493,6 +495,12 @@ impl Default for TerminalContent { } } +#[derive(PartialEq, Eq)] +pub enum SelectionPhase { + Selecting, + Ended, +} + pub struct Terminal { pty_tx: Notifier, term: Arc>>, @@ -511,6 +519,8 @@ pub struct Terminal { shell_fd: u32, foreground_process_info: Option, scroll_px: f32, + next_link_id: usize, + selection_phase: SelectionPhase, } impl Terminal { @@ -654,7 +664,7 @@ impl Terminal { } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), InternalEvent::Hyperlink(position, open) => { - self.last_content.last_hovered_hyperlink = None; + let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); let point = grid_point( *position, @@ -668,13 +678,37 @@ impl Terminal { if *open { open_uri(&url).log_err(); } else { - self.last_content.last_hovered_hyperlink = Some((url, url_match)); + self.update_hyperlink(prev_hyperlink, url, url_match); } } } } } + fn update_hyperlink( + &mut self, + prev_hyperlink: Option<(String, RangeInclusive, usize)>, + url: String, + url_match: RangeInclusive, + ) { + 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 } @@ -846,7 +880,8 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - self.last_content.last_hovered_hyperlink = None; + 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) { @@ -862,19 +897,26 @@ impl Terminal { self.pty_tx.notify(bytes); } } - } else if e.cmd { - self.fill_hyperlink(Some(position)); + } else { + self.fill_hyperlink(Some(position), prev_hyperlink); } } - fn fill_hyperlink(&mut self, position: Option) { - if let Some(position) = position { + fn fill_hyperlink( + &mut self, + position: Option, + prev_hyperlink: Option<(String, RangeInclusive, 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 self.last_content.cells[min_index - 1].hyperlink() == link { + if min_index >= 1 && self.last_content.cells[min_index - 1].hyperlink() == link + { min_index = min_index - 1; } else { break; @@ -882,21 +924,24 @@ impl Terminal { } let mut max_index = content_index; + let len = self.last_content.cells.len(); loop { - if self.last_content.cells[max_index + 1].hyperlink() == link { + if max_index < len - 1 + && self.last_content.cells[max_index + 1].hyperlink() == link + { max_index = max_index + 1; } else { break; } } - self.last_content.last_hovered_hyperlink = link.map(|link| { - ( - link.uri().to_owned(), - self.last_content.cells[min_index].point - ..=self.last_content.cells[max_index].point, - ) - }); + 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)); @@ -909,6 +954,7 @@ impl Terminal { 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 @@ -969,7 +1015,9 @@ impl Terminal { pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { - if e.cmd { + if self.last_content.last_hovered_hyperlink.is_some() + && self.last_content.selection.is_none() + { 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(); @@ -1021,6 +1069,7 @@ impl Terminal { // so let's do that here self.copy(); } + self.selection_phase = SelectionPhase::Ended; self.last_mouse = None; } @@ -1061,10 +1110,10 @@ impl Terminal { } pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool { - self.last_content.last_hovered_hyperlink = None; + let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); if cmd { - self.fill_hyperlink(self.last_mouse_position); + self.fill_hyperlink(self.last_mouse_position, prev_hyperlink); true } else { false diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 9c47e33da3f61372105ee2e7fc43b4c8189110b4..10d6d279c4fca52bbe78747350180441f3c1a9d7 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -7,7 +7,7 @@ use alacritty_terminal::{ use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ color::Color, - elements::{Overlay, Tooltip}, + elements::{Empty, Overlay}, fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight}, geometry::{ rect::RectF, @@ -15,7 +15,7 @@ use gpui::{ }, serde_json::json, text_layout::{Line, RunStyle}, - Axis, Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, + Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache, WeakModelHandle, WeakViewHandle, }; @@ -324,7 +324,6 @@ impl TerminalElement { .unwrap_or_default(); if indexed.cell.hyperlink().is_some() { - underline.squiggly = true; if underline.thickness == OrderedFloat(0.) { underline.thickness = OrderedFloat(1.); } @@ -594,51 +593,34 @@ impl Element for TerminalElement { }; let terminal_handle = self.terminal.upgrade(cx).unwrap(); - let (last_hovered_hyperlink, last_mouse) = - terminal_handle.update(cx.app, |terminal, cx| { - terminal.set_size(dimensions); - terminal.try_sync(cx); - ( - terminal.last_content.last_hovered_hyperlink.clone(), - terminal.last_mouse_position, - ) - }); + let last_hovered_hyperlink = terminal_handle.update(cx.app, |terminal, cx| { + terminal.set_size(dimensions); + 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, _)| { - last_mouse.and_then(|last_mouse| { - view_handle.upgrade(cx).map(|handle| { - let mut tooltip = cx.render(&handle, |_, cx| { - // TODO: Use the correct dynamic line height - // let mut collapsed_tooltip = Tooltip::render_tooltip( - // uri.clone(), - // tooltip_style.clone(), - // None, - // false, - // ) - // .boxed(); - - Overlay::new( - Tooltip::render_tooltip(uri, tooltip_style, None, false) - .constrained() - .with_height(text_style.line_height(cx.font_cache())) - // .dynamically(move |constraint, cx| { - // SizeConstraint::strict_along( - // Axis::Vertical, - // collapsed_tooltip.layout(constraint, cx).y(), - // ) - // }) - .boxed(), - ) - .with_fit_mode(gpui::elements::OverlayFitMode::SwitchAnchor) - .with_anchor_position(last_mouse) - .boxed() - }); + 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::(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 - }) + tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx); + tooltip }) + // }) }); let TerminalContent { @@ -672,7 +654,7 @@ impl Element for TerminalElement { self.modal, last_hovered_hyperlink .as_ref() - .map(|(_, range)| (link_style, range)), + .map(|(_, range, _)| (link_style, range)), ); //Layout cursor. Rectangle is used for IME, so we should lay it out even @@ -822,12 +804,7 @@ impl Element for TerminalElement { } if let Some(element) = &mut layout.hyperlink_tooltip { - element.paint( - visible_bounds.lower_left() - - vec2f(-layout.size.cell_width, layout.size.line_height), - visible_bounds, - cx, - ) + element.paint(origin, visible_bounds, cx) } }); } From 550ae40ff5a0dac472d88cd657e27450f7b7d105 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 16:36:08 -0700 Subject: [PATCH 13/13] Slightly improved left click handling --- crates/terminal/src/terminal.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 56617001cfc264a8bf20f7c50fbae239cac1b800..0a518d2b15a7decf564488250b32cad11f000414 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1015,8 +1015,7 @@ impl Terminal { pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { - if self.last_content.last_hovered_hyperlink.is_some() - && self.last_content.selection.is_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() { @@ -1025,7 +1024,10 @@ impl Terminal { self.events .push_back(InternalEvent::Hyperlink(position, true)); } - } else { + } + + // Selections + { let point = grid_point( position, self.last_content.size,