Add editor::OpenUrl action and bind to `gx` in Vim mode (#7972)

Thorsten Ball created

This adds one feature I've been missing a lot in Vim mode: `gx` to open
the URL under the cursor.

Technically, in Vim, `gx` opens more "paths", not just URLs, but I think
this is a good start.

Release Notes:

- Added `gx` to Vim mode to open the URL under the cursor.

Demo:


https://github.com/zed-industries/zed/assets/1185253/6a19490d-b61d-40b7-93e8-4819599f6977

Change summary

assets/keymaps/vim.json          |  1 +
crates/editor/src/actions.rs     |  1 +
crates/editor/src/editor.rs      | 24 ++++++++++++++++++++++++
crates/editor/src/element.rs     |  1 +
crates/editor/src/hover_links.rs |  2 +-
5 files changed, 28 insertions(+), 1 deletion(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -125,6 +125,7 @@
       "g shift-t": "pane::ActivatePrevItem",
       "g d": "editor::GoToDefinition",
       "g shift-d": "editor::GoToTypeDefinition",
+      "g x": "editor::OpenUrl",
       "g n": "vim::SelectNext",
       "g shift-n": "vim::SelectPrevious",
       "g >": [

crates/editor/src/actions.rs 🔗

@@ -165,6 +165,7 @@ gpui::actions!(
         GoToPrevHunk,
         GoToTypeDefinition,
         GoToTypeDefinitionSplit,
+        OpenUrl,
         HalfPageDown,
         HalfPageUp,
         Hover,

crates/editor/src/editor.rs 🔗

@@ -123,6 +123,8 @@ use util::{maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::Toast;
 use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace};
 
+use crate::hover_links::find_url;
+
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
@@ -7368,6 +7370,28 @@ impl Editor {
         .detach_and_log_err(cx);
     }
 
+    pub fn open_url(&mut self, _: &OpenUrl, cx: &mut ViewContext<Self>) {
+        let position = self.selections.newest_anchor().head();
+        let Some((buffer, buffer_position)) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(position.clone(), cx)
+        else {
+            return;
+        };
+
+        cx.spawn(|editor, mut cx| async move {
+            if let Some((_, url)) = find_url(&buffer, buffer_position, cx.clone()) {
+                editor.update(&mut cx, |_, cx| {
+                    cx.open_url(&url);
+                })
+            } else {
+                Ok(())
+            }
+        })
+        .detach();
+    }
+
     pub fn navigate_to_hover_links(
         &mut self,
         mut definitions: Vec<HoverLink>,

crates/editor/src/element.rs 🔗

@@ -262,6 +262,7 @@ impl EditorElement {
         register_action(view, cx, Editor::go_to_definition_split);
         register_action(view, cx, Editor::go_to_type_definition);
         register_action(view, cx, Editor::go_to_type_definition_split);
+        register_action(view, cx, Editor::open_url);
         register_action(view, cx, Editor::fold);
         register_action(view, cx, Editor::fold_at);
         register_action(view, cx, Editor::unfold_lines);

crates/editor/src/hover_links.rs 🔗

@@ -569,7 +569,7 @@ pub fn show_link_definition(
     editor.hovered_link_state = Some(hovered_link_state);
 }
 
-fn find_url(
+pub(crate) fn find_url(
     buffer: &Model<language::Buffer>,
     position: text::Anchor,
     mut cx: AsyncWindowContext,