Finished terminal hyperlinks for now

Mikayla Maki created

Change summary

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(-)

Detailed changes

crates/gpui/src/elements/overlay.rs 🔗

@@ -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 => {

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<Point>)>,
+    last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, 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<FairMutex<Term<ZedListener>>>,
@@ -511,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 {
@@ -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<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
     }
@@ -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<Vector2F>) {
-        if let Some(position) = position {
+    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 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

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::<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
-                })
+                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)
             }
         });
     }