Detect and open URLs properly

Kirill Bulatov created

Change summary

crates/terminal/src/terminal.rs              | 92 ++++++++++++++-------
crates/terminal_view/src/terminal_element.rs | 18 ++-
crates/terminal_view/src/terminal_view.rs    | 14 +-
3 files changed, 79 insertions(+), 45 deletions(-)

Detailed changes

crates/terminal/src/terminal.rs 🔗

@@ -74,7 +74,7 @@ const DEBUG_LINE_HEIGHT: f32 = 5.;
 
 lazy_static! {
     // Regex Copied from alacritty's ui_config.rs
-    pub 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();
+    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();
 
     static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-]+").unwrap();
 }
@@ -89,7 +89,10 @@ pub enum Event {
     Wakeup,
     BlinkChanged,
     SelectionsChanged,
-    Open(String),
+    Open {
+        is_url: bool,
+        maybe_url_or_path: String,
+    },
 }
 
 #[derive(Clone)]
@@ -592,7 +595,14 @@ pub struct TerminalContent {
     pub cursor: RenderableCursor,
     pub cursor_char: char,
     pub size: TerminalSize,
-    pub last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
+    pub last_hovered_word: Option<HoveredWord>,
+}
+
+#[derive(Clone)]
+pub struct HoveredWord {
+    pub word: String,
+    pub word_match: RangeInclusive<Point>,
+    pub id: usize,
 }
 
 impl Default for TerminalContent {
@@ -609,7 +619,7 @@ impl Default for TerminalContent {
             },
             cursor_char: Default::default(),
             size: Default::default(),
-            last_hovered_hyperlink: None,
+            last_hovered_word: None,
         }
     }
 }
@@ -626,7 +636,7 @@ pub struct Terminal {
     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
+    /// This is only used for terminal hovered word checking
     last_mouse_position: Option<Vector2F>,
     pub matches: Vec<RangeInclusive<Point>>,
     pub last_content: TerminalContent,
@@ -773,7 +783,7 @@ impl Terminal {
             }
             InternalEvent::Scroll(scroll) => {
                 term.scroll_display(*scroll);
-                self.refresh_hyperlink();
+                self.refresh_hovered_word();
             }
             InternalEvent::SetSelection(selection) => {
                 term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
@@ -808,10 +818,10 @@ impl Terminal {
             }
             InternalEvent::ScrollToPoint(point) => {
                 term.scroll_to_point(*point);
-                self.refresh_hyperlink();
+                self.refresh_hovered_word();
             }
             InternalEvent::FindHyperlink(position, open) => {
-                let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
+                let prev_hovered_word = self.last_content.last_hovered_word.take();
 
                 let point = grid_point(
                     *position,
@@ -851,41 +861,57 @@ impl Terminal {
                     let url = link.unwrap().uri().to_owned();
                     let url_match = min_index..=max_index;
 
-                    Some((url, url_match))
-                } else if let Some(url_match) = regex_match_at(term, point, &WORD_REGEX) {
-                    let url = term.bounds_to_string(*url_match.start(), *url_match.end());
+                    Some((url, true, url_match))
+                } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) {
+                    let maybe_url_or_path =
+                        term.bounds_to_string(*word_match.start(), *word_match.end());
+                    let is_url = regex_match_at(term, point, &URL_REGEX).is_some();
 
-                    Some((url, url_match))
+                    Some((maybe_url_or_path, is_url, word_match))
                 } else {
                     None
                 };
 
-                if let Some((url, url_match)) = found_url {
+                if let Some((maybe_url_or_path, is_url, url_match)) = found_url {
                     if *open {
-                        cx.emit(Event::Open(url))
+                        cx.emit(Event::Open {
+                            is_url,
+                            maybe_url_or_path,
+                        })
                     } else {
-                        self.update_hyperlink(prev_hyperlink, url, url_match);
+                        self.update_selected_word(prev_hovered_word, maybe_url_or_path, url_match);
                     }
                 }
             }
         }
     }
 
-    fn update_hyperlink(
+    fn update_selected_word(
         &mut self,
-        prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
-        url: String,
-        url_match: RangeInclusive<Point>,
+        prev_word: Option<HoveredWord>,
+        word: String,
+        word_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));
+        if let Some(prev_word) = prev_word {
+            if prev_word.word == word && prev_word.word_match == word_match {
+                self.last_content.last_hovered_word = Some(HoveredWord {
+                    word,
+                    word_match,
+                    id: prev_word.id,
+                });
             } else {
-                self.last_content.last_hovered_hyperlink =
-                    Some((url, url_match, self.next_link_id()));
+                self.last_content.last_hovered_word = Some(HoveredWord {
+                    word,
+                    word_match,
+                    id: self.next_link_id(),
+                });
             }
         } else {
-            self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id()));
+            self.last_content.last_hovered_word = Some(HoveredWord {
+                word,
+                word_match,
+                id: self.next_link_id(),
+            });
         }
     }
 
@@ -974,9 +1000,9 @@ impl Terminal {
         if changed {
             self.cmd_pressed = cmd;
             if cmd {
-                self.refresh_hyperlink();
+                self.refresh_hovered_word();
             } else {
-                self.last_content.last_hovered_hyperlink.take();
+                self.last_content.last_hovered_word.take();
             }
         }
 
@@ -1054,7 +1080,7 @@ impl Terminal {
             cursor: content.cursor,
             cursor_char: term.grid()[content.cursor.point].c,
             size: last_content.size,
-            last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
+            last_hovered_word: last_content.last_hovered_word.clone(),
         }
     }
 
@@ -1109,13 +1135,13 @@ impl Terminal {
                 }
             }
         } else if self.cmd_pressed {
-            self.hyperlink_from_position(Some(position));
+            self.word_from_position(Some(position));
         }
     }
 
-    fn hyperlink_from_position(&mut self, position: Option<Vector2F>) {
+    fn word_from_position(&mut self, position: Option<Vector2F>) {
         if self.selection_phase == SelectionPhase::Selecting {
-            self.last_content.last_hovered_hyperlink = None;
+            self.last_content.last_hovered_word = None;
         } else if let Some(position) = position {
             self.events
                 .push_back(InternalEvent::FindHyperlink(position, false));
@@ -1274,8 +1300,8 @@ impl Terminal {
         }
     }
 
-    pub fn refresh_hyperlink(&mut self) {
-        self.hyperlink_from_position(self.last_mouse_position);
+    pub fn refresh_hovered_word(&mut self) {
+        self.word_from_position(self.last_mouse_position);
     }
 
     fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {

crates/terminal_view/src/terminal_element.rs 🔗

@@ -583,17 +583,23 @@ impl Element<TerminalView> for TerminalElement {
         let last_hovered_hyperlink = terminal_handle.update(cx, |terminal, cx| {
             terminal.set_size(dimensions);
             terminal.try_sync(cx);
-            terminal.last_content.last_hovered_hyperlink.clone()
+            terminal.last_content.last_hovered_word.clone()
         });
 
-        let hyperlink_tooltip = last_hovered_hyperlink.map(|(uri, _, id)| {
+        let hyperlink_tooltip = last_hovered_hyperlink.map(|hovered_word| {
             let mut tooltip = Overlay::new(
                 Empty::new()
                     .contained()
                     .constrained()
                     .with_width(dimensions.width())
                     .with_height(dimensions.height())
-                    .with_tooltip::<TerminalElement>(id, uri, None, tooltip_style, cx),
+                    .with_tooltip::<TerminalElement>(
+                        hovered_word.id,
+                        hovered_word.word,
+                        None,
+                        tooltip_style,
+                        cx,
+                    ),
             )
             .with_position_mode(gpui::elements::OverlayPositionMode::Local)
             .into_any();
@@ -613,7 +619,7 @@ impl Element<TerminalView> for TerminalElement {
             cursor_char,
             selection,
             cursor,
-            last_hovered_hyperlink,
+            last_hovered_word,
             ..
         } = { &terminal_handle.read(cx).last_content };
 
@@ -634,9 +640,9 @@ impl Element<TerminalView> for TerminalElement {
             &terminal_theme,
             cx.text_layout_cache(),
             cx.font_cache(),
-            last_hovered_hyperlink
+            last_hovered_word
                 .as_ref()
-                .map(|(_, range, _)| (link_style, range)),
+                .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
         );
 
         //Layout cursor. Rectangle is used for IME, so we should lay it out even

crates/terminal_view/src/terminal_view.rs 🔗

@@ -166,10 +166,11 @@ impl TerminalView {
                         .detach();
                 }
             }
-            Event::Open(maybe_url_or_path) => {
-                // TODO kb, what is the API for this?
-                // terminal::URL_REGEX.matches(maybe_url_or_path)
-                if maybe_url_or_path.starts_with("http") {
+            Event::Open {
+                is_url,
+                maybe_url_or_path,
+            } => {
+                if *is_url {
                     cx.platform().open_url(maybe_url_or_path);
                 } else if let Some(workspace) = workspace.upgrade(cx) {
                     let path_like =
@@ -180,10 +181,11 @@ impl TerminalView {
                     let maybe_path = path_like.path_like;
                     workspace.update(cx, |workspace, cx| {
                         if false { //&& workspace.contains_path() {
-                             //
+                             // TODO kb
                         } else if maybe_path.exists() {
+                            let visible = maybe_path.is_dir();
                             workspace
-                                .open_abs_path(maybe_path, true, cx)
+                                .open_abs_path(maybe_path, visible, cx)
                                 .detach_and_log_err(cx);
                         }
                     });