From 5318f529de7960c6baa1cd3494af8cf8131beabc Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Fri, 13 Dec 2024 11:45:21 +0530 Subject: [PATCH] Improve editor open URL command to open the selected portion of URL (#21825) Closes #21718 Just like in Vim, if a URL is selected, it opens exactly that portion of the URL. Otherwise, if only the cursor is on a URL, it opens the entire URL. Zed currently does the latter. This PR also adds support for the former. https://github.com/user-attachments/assets/8bdd2952-ceec-487c-b27a-5cea4258eb03 Release Notes: - Updated the `editor: open url` to also handle the selected portion of a URL. --- crates/editor/src/editor.rs | 35 ++++++++++++++----- crates/editor/src/hover_links.rs | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 38b34892323e3e141f55bac43285fa9e85c266d9..7b5008a0b812a7cefa2830d96d5ed1a6bb5675a5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -176,7 +176,7 @@ use workspace::{ }; use workspace::{Item as WorkspaceItem, OpenInTerminal, OpenTerminal, TabBarSettings, Toast}; -use crate::hover_links::find_url; +use crate::hover_links::{find_url, find_url_from_range}; use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; pub const FILE_HEADER_HEIGHT: u32 = 2; @@ -9293,23 +9293,42 @@ impl Editor { } pub fn open_url(&mut self, _: &OpenUrl, cx: &mut ViewContext) { - let position = self.selections.newest_anchor().head(); - let Some((buffer, buffer_position)) = - self.buffer.read(cx).text_anchor_for_position(position, cx) + let selection = self.selections.newest_anchor(); + let head = selection.head(); + let tail = selection.tail(); + + let Some((buffer, start_position)) = + self.buffer.read(cx).text_anchor_for_position(head, cx) else { return; }; - cx.spawn(|editor, mut cx| async move { - if let Some((_, url)) = find_url(&buffer, buffer_position, cx.clone()) { + let end_position = if head != tail { + let Some((_, pos)) = self.buffer.read(cx).text_anchor_for_position(tail, cx) else { + return; + }; + Some(pos) + } else { + None + }; + + let url_finder = cx.spawn(|editor, mut cx| async move { + let url = if let Some(end_pos) = end_position { + find_url_from_range(&buffer, start_position..end_pos, cx.clone()) + } else { + find_url(&buffer, start_position, cx.clone()).map(|(_, url)| url) + }; + + if let Some(url) = url { editor.update(&mut cx, |_, cx| { cx.open_url(&url); }) } else { Ok(()) } - }) - .detach(); + }); + + url_finder.detach(); } pub fn open_file(&mut self, _: &OpenFile, cx: &mut ViewContext) { diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 0973f59babf6d8a112db601939fc5f82b271c69e..75218ff1c8bfef6b658417c7b5e427b55ee1acb9 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -694,6 +694,65 @@ pub(crate) fn find_url( None } +pub(crate) fn find_url_from_range( + buffer: &Model, + range: Range, + mut cx: AsyncWindowContext, +) -> Option { + const LIMIT: usize = 2048; + + let Ok(snapshot) = buffer.update(&mut cx, |buffer, _| buffer.snapshot()) else { + return None; + }; + + let start_offset = range.start.to_offset(&snapshot); + let end_offset = range.end.to_offset(&snapshot); + + let mut token_start = start_offset.min(end_offset); + let mut token_end = start_offset.max(end_offset); + + let range_len = token_end - token_start; + + if range_len >= LIMIT { + return None; + } + + // Skip leading whitespace + for ch in snapshot.chars_at(token_start).take(range_len) { + if !ch.is_whitespace() { + break; + } + token_start += ch.len_utf8(); + } + + // Skip trailing whitespace + for ch in snapshot.reversed_chars_at(token_end).take(range_len) { + if !ch.is_whitespace() { + break; + } + token_end -= ch.len_utf8(); + } + + if token_start >= token_end { + return None; + } + + let text = snapshot + .text_for_range(token_start..token_end) + .collect::(); + + let mut finder = LinkFinder::new(); + finder.kinds(&[LinkKind::Url]); + + if let Some(link) = finder.links(&text).next() { + if link.start() == 0 && link.end() == text.len() { + return Some(link.as_str().to_string()); + } + } + + None +} + pub(crate) async fn find_file( buffer: &Model, project: Option>,