Detailed changes
@@ -1915,7 +1915,9 @@ impl Project {
return;
}
- let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+ let abs_path = file.abs_path(cx);
+ let uri = lsp::Url::from_file_path(&abs_path)
+ .unwrap_or_else(|()| panic!("Failed to register file {abs_path:?}"));
let initial_snapshot = buffer.text_snapshot();
let language = buffer.language().cloned();
let worktree_id = file.worktree_id(cx);
@@ -51,7 +51,7 @@ use gpui::{
fonts,
geometry::vector::{vec2f, Vector2F},
keymap_matcher::Keystroke,
- platform::{MouseButton, MouseMovedEvent, TouchPhase},
+ platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase},
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
AppContext, ClipboardItem, Entity, ModelContext, Task,
};
@@ -72,14 +72,15 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
const DEBUG_CELL_WIDTH: f32 = 5.;
const DEBUG_LINE_HEIGHT: f32 = 5.;
-// Regex Copied from alacritty's ui_config.rs
-
lazy_static! {
+ // Regex Copied from alacritty's ui_config.rs
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();
}
///Upward flowing events, for changing the title and such
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Debug)]
pub enum Event {
TitleChanged,
BreadcrumbsChanged,
@@ -88,6 +89,18 @@ pub enum Event {
Wakeup,
BlinkChanged,
SelectionsChanged,
+ NewNavigationTarget(Option<MaybeNavigationTarget>),
+ Open(MaybeNavigationTarget),
+}
+
+/// A string inside terminal, potentially useful as a URI that can be opened.
+#[derive(Clone, Debug)]
+pub enum MaybeNavigationTarget {
+ /// HTTP, git, etc. string determined by the [`URL_REGEX`] regex.
+ Url(String),
+ /// File system path, absolute or relative, existing or not.
+ /// Might have line and column number(s) attached as `file.rs:1:23`
+ PathLike(String),
}
#[derive(Clone)]
@@ -493,6 +506,8 @@ impl TerminalBuilder {
last_mouse_position: None,
next_link_id: 0,
selection_phase: SelectionPhase::Ended,
+ cmd_pressed: false,
+ hovered_word: false,
};
Ok(TerminalBuilder {
@@ -589,7 +604,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 {
@@ -606,7 +628,7 @@ impl Default for TerminalContent {
},
cursor_char: Default::default(),
size: Default::default(),
- last_hovered_hyperlink: None,
+ last_hovered_word: None,
}
}
}
@@ -623,7 +645,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,
@@ -637,6 +659,8 @@ pub struct Terminal {
scroll_px: f32,
next_link_id: usize,
selection_phase: SelectionPhase,
+ cmd_pressed: bool,
+ hovered_word: bool,
}
impl Terminal {
@@ -769,7 +793,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());
@@ -804,20 +828,20 @@ 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,
self.last_content.size,
term.grid().display_offset(),
)
- .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor);
+ .grid_clamp(term, alacritty_terminal::index::Boundary::Grid);
let link = term.grid().index(point).hyperlink();
- let found_url = if link.is_some() {
+ let found_word = if link.is_some() {
let mut min_index = point;
loop {
let new_min_index =
@@ -847,42 +871,78 @@ 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, &URL_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 *open {
- cx.platform().open_url(url.as_str());
- } else {
- self.update_hyperlink(prev_hyperlink, url, url_match);
+ match found_word {
+ Some((maybe_url_or_path, is_url, url_match)) => {
+ if *open {
+ let target = if is_url {
+ MaybeNavigationTarget::Url(maybe_url_or_path)
+ } else {
+ MaybeNavigationTarget::PathLike(maybe_url_or_path)
+ };
+ cx.emit(Event::Open(target));
+ } else {
+ self.update_selected_word(
+ prev_hovered_word,
+ url_match,
+ maybe_url_or_path,
+ is_url,
+ cx,
+ );
+ }
+ self.hovered_word = true;
+ }
+ None => {
+ if self.hovered_word {
+ cx.emit(Event::NewNavigationTarget(None));
+ }
+ self.hovered_word = false;
}
}
}
}
}
- 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_match: RangeInclusive<Point>,
+ word: String,
+ is_url: bool,
+ cx: &mut ModelContext<Self>,
) {
- 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()));
+ 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,
+ });
+ return;
}
- } 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.clone(),
+ word_match,
+ id: self.next_link_id(),
+ });
+ let navigation_target = if is_url {
+ MaybeNavigationTarget::Url(word)
+ } else {
+ MaybeNavigationTarget::PathLike(word)
+ };
+ cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
}
fn next_link_id(&mut self) -> usize {
@@ -964,6 +1024,15 @@ impl Terminal {
}
}
+ pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool {
+ let changed = self.cmd_pressed != modifiers.cmd;
+ if !self.cmd_pressed && modifiers.cmd {
+ self.refresh_hovered_word();
+ }
+ self.cmd_pressed = modifiers.cmd;
+ changed
+ }
+
///Paste text into the terminal
pub fn paste(&mut self, text: &str) {
let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
@@ -1035,7 +1104,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(),
}
}
@@ -1089,14 +1158,14 @@ impl Terminal {
self.pty_tx.notify(bytes);
}
}
- } else {
- self.hyperlink_from_position(Some(position));
+ } else if self.cmd_pressed {
+ 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));
@@ -1208,7 +1277,7 @@ impl Terminal {
let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
cx.platform().open_url(link.uri());
- } else {
+ } else if self.cmd_pressed {
self.events
.push_back(InternalEvent::FindHyperlink(position, true));
}
@@ -1255,8 +1324,8 @@ impl Terminal {
}
}
- pub fn refresh_hyperlink(&mut self) {
- self.hyperlink_from_position(self.last_mouse_position);
+ 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> {
@@ -1334,6 +1403,10 @@ impl Terminal {
})
.unwrap_or_else(|| "Terminal".to_string())
}
+
+ pub fn can_navigate_to_selected_word(&self) -> bool {
+ self.cmd_pressed && self.hovered_word
+ }
}
impl Drop for Terminal {
@@ -163,6 +163,7 @@ pub struct TerminalElement {
terminal: WeakModelHandle<Terminal>,
focused: bool,
cursor_visible: bool,
+ can_navigate_to_selected_word: bool,
}
impl TerminalElement {
@@ -170,11 +171,13 @@ impl TerminalElement {
terminal: WeakModelHandle<Terminal>,
focused: bool,
cursor_visible: bool,
+ can_navigate_to_selected_word: bool,
) -> TerminalElement {
TerminalElement {
terminal,
focused,
cursor_visible,
+ can_navigate_to_selected_word,
}
}
@@ -580,20 +583,30 @@ impl Element<TerminalView> for TerminalElement {
let background_color = terminal_theme.background;
let terminal_handle = self.terminal.upgrade(cx).unwrap();
- let last_hovered_hyperlink = terminal_handle.update(cx, |terminal, cx| {
+ let last_hovered_word = terminal_handle.update(cx, |terminal, cx| {
terminal.set_size(dimensions);
terminal.try_sync(cx);
- terminal.last_content.last_hovered_hyperlink.clone()
+ if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() {
+ terminal.last_content.last_hovered_word.clone()
+ } else {
+ None
+ }
});
- let hyperlink_tooltip = last_hovered_hyperlink.map(|(uri, _, id)| {
+ let hyperlink_tooltip = last_hovered_word.clone().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 +626,6 @@ impl Element<TerminalView> for TerminalElement {
cursor_char,
selection,
cursor,
- last_hovered_hyperlink,
..
} = { &terminal_handle.read(cx).last_content };
@@ -634,9 +646,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
@@ -261,10 +261,14 @@ impl TerminalPanel {
.create_terminal(working_directory, window_id, cx)
.log_err()
}) {
- let terminal =
- Box::new(cx.add_view(|cx| {
- TerminalView::new(terminal, workspace.database_id(), cx)
- }));
+ let terminal = Box::new(cx.add_view(|cx| {
+ TerminalView::new(
+ terminal,
+ workspace.weak_handle(),
+ workspace.database_id(),
+ cx,
+ )
+ }));
pane.update(cx, |pane, cx| {
let focus = pane.has_focus();
pane.add_item(terminal, true, focus, None, cx);
@@ -3,18 +3,21 @@ pub mod terminal_element;
pub mod terminal_panel;
use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
+use anyhow::Context;
use context_menu::{ContextMenu, ContextMenuItem};
use dirs::home_dir;
+use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
actions,
elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack},
geometry::vector::Vector2F,
impl_actions,
keymap_matcher::{KeymapContext, Keystroke},
- platform::KeyDownEvent,
+ platform::{KeyDownEvent, ModifiersChangedEvent},
AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
+use language::Bias;
use project::{LocalWorktree, Project};
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
@@ -30,9 +33,9 @@ use terminal::{
index::Point,
term::{search::RegexSearch, TermMode},
},
- Event, Terminal, TerminalBlink, WorkingDirectory,
+ Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory,
};
-use util::ResultExt;
+use util::{paths::PathLikeWithPosition, ResultExt};
use workspace::{
item::{BreadcrumbText, Item, ItemEvent},
notifications::NotifyResultExt,
@@ -90,6 +93,7 @@ pub struct TerminalView {
blinking_on: bool,
blinking_paused: bool,
blink_epoch: usize,
+ can_navigate_to_selected_word: bool,
workspace_id: WorkspaceId,
}
@@ -117,19 +121,27 @@ impl TerminalView {
.notify_err(workspace, cx);
if let Some(terminal) = terminal {
- let view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
+ let view = cx.add_view(|cx| {
+ TerminalView::new(
+ terminal,
+ workspace.weak_handle(),
+ workspace.database_id(),
+ cx,
+ )
+ });
workspace.add_item(Box::new(view), cx)
}
}
pub fn new(
terminal: ModelHandle<Terminal>,
+ workspace: WeakViewHandle<Workspace>,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Self {
let view_id = cx.view_id();
cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
- cx.subscribe(&terminal, |this, _, event, cx| match event {
+ cx.subscribe(&terminal, move |this, _, event, cx| match event {
Event::Wakeup => {
if !cx.is_self_focused() {
this.has_new_content = true;
@@ -158,7 +170,63 @@ impl TerminalView {
.detach();
}
}
- _ => cx.emit(*event),
+ Event::NewNavigationTarget(maybe_navigation_target) => {
+ this.can_navigate_to_selected_word = match maybe_navigation_target {
+ Some(MaybeNavigationTarget::Url(_)) => true,
+ Some(MaybeNavigationTarget::PathLike(maybe_path)) => {
+ !possible_open_targets(&workspace, maybe_path, cx).is_empty()
+ }
+ None => false,
+ }
+ }
+ Event::Open(maybe_navigation_target) => match maybe_navigation_target {
+ MaybeNavigationTarget::Url(url) => cx.platform().open_url(url),
+ MaybeNavigationTarget::PathLike(maybe_path) => {
+ if !this.can_navigate_to_selected_word {
+ return;
+ }
+ let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
+ if let Some(path) = potential_abs_paths.into_iter().next() {
+ let visible = path.path_like.is_dir();
+ let task_workspace = workspace.clone();
+ cx.spawn(|_, mut cx| async move {
+ let opened_item = task_workspace
+ .update(&mut cx, |workspace, cx| {
+ workspace.open_abs_path(path.path_like, visible, cx)
+ })
+ .context("workspace update")?
+ .await
+ .context("workspace update")?;
+ if let Some(row) = path.row {
+ let col = path.column.unwrap_or(0);
+ if let Some(active_editor) = opened_item.downcast::<Editor>() {
+ active_editor
+ .downgrade()
+ .update(&mut cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx).display_snapshot;
+ let point = snapshot.buffer_snapshot.clip_point(
+ language::Point::new(
+ row.saturating_sub(1),
+ col.saturating_sub(1),
+ ),
+ Bias::Left,
+ );
+ editor.change_selections(
+ Some(Autoscroll::center()),
+ cx,
+ |s| s.select_ranges([point..point]),
+ );
+ })
+ .log_err();
+ }
+ }
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+ },
+ _ => cx.emit(event.clone()),
})
.detach();
@@ -171,6 +239,7 @@ impl TerminalView {
blinking_on: false,
blinking_paused: false,
blink_epoch: 0,
+ can_navigate_to_selected_word: false,
workspace_id,
}
}
@@ -344,6 +413,40 @@ impl TerminalView {
}
}
+fn possible_open_targets(
+ workspace: &WeakViewHandle<Workspace>,
+ maybe_path: &String,
+ cx: &mut ViewContext<'_, '_, TerminalView>,
+) -> Vec<PathLikeWithPosition<PathBuf>> {
+ let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| {
+ Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf())
+ })
+ .expect("infallible");
+ let maybe_path = path_like.path_like;
+ let potential_abs_paths = if maybe_path.is_absolute() {
+ vec![maybe_path]
+ } else if let Some(workspace) = workspace.upgrade(cx) {
+ workspace.update(cx, |workspace, cx| {
+ workspace
+ .worktrees(cx)
+ .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
+ .collect()
+ })
+ } else {
+ Vec::new()
+ };
+
+ potential_abs_paths
+ .into_iter()
+ .filter(|path| path.exists())
+ .map(|path| PathLikeWithPosition {
+ path_like: path,
+ row: path_like.row,
+ column: path_like.column,
+ })
+ .collect()
+}
+
pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
let searcher = match query {
project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query),
@@ -372,6 +475,7 @@ impl View for TerminalView {
terminal_handle,
focused,
self.should_show_cursor(focused, cx),
+ self.can_navigate_to_selected_word,
)
.contained(),
)
@@ -393,6 +497,20 @@ impl View for TerminalView {
cx.notify();
}
+ fn modifiers_changed(
+ &mut self,
+ event: &ModifiersChangedEvent,
+ cx: &mut ViewContext<Self>,
+ ) -> bool {
+ let handled = self
+ .terminal()
+ .update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
+ if handled {
+ cx.notify();
+ }
+ handled
+ }
+
fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
self.clear_bel(cx);
self.pause_cursor_blinking(cx);
@@ -618,7 +736,7 @@ impl Item for TerminalView {
project.create_terminal(cwd, window_id, cx)
})?;
Ok(pane.update(&mut cx, |_, cx| {
- cx.add_view(|cx| TerminalView::new(terminal, workspace_id, cx))
+ cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
})?)
})
}
@@ -895,7 +895,14 @@ pub fn dock_default_item_factory(
})
.notify_err(workspace, cx)?;
- let terminal_view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
+ let terminal_view = cx.add_view(|cx| {
+ TerminalView::new(
+ terminal,
+ workspace.weak_handle(),
+ workspace.database_id(),
+ cx,
+ )
+ });
Some(Box::new(terminal_view))
}