Detect and open URLs properly

This commit is contained in:
Kirill Bulatov 2023-07-15 01:11:20 +03:00
parent 23f25562b5
commit 6123c67de9
3 changed files with 79 additions and 45 deletions

View file

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

View file

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

View file

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