Finished terminal hyperlinks for now
This commit is contained in:
parent
d2d49633f1
commit
4bc0afdafa
3 changed files with 124 additions and 73 deletions
|
@ -14,6 +14,7 @@ pub struct Overlay {
|
||||||
anchor_position: Option<Vector2F>,
|
anchor_position: Option<Vector2F>,
|
||||||
anchor_corner: AnchorCorner,
|
anchor_corner: AnchorCorner,
|
||||||
fit_mode: OverlayFitMode,
|
fit_mode: OverlayFitMode,
|
||||||
|
position_mode: OverlayPositionMode,
|
||||||
hoverable: bool,
|
hoverable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +25,12 @@ pub enum OverlayFitMode {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum OverlayPositionMode {
|
||||||
|
Window,
|
||||||
|
Local,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AnchorCorner {
|
pub enum AnchorCorner {
|
||||||
TopLeft,
|
TopLeft,
|
||||||
|
@ -73,6 +80,7 @@ impl Overlay {
|
||||||
anchor_position: None,
|
anchor_position: None,
|
||||||
anchor_corner: AnchorCorner::TopLeft,
|
anchor_corner: AnchorCorner::TopLeft,
|
||||||
fit_mode: OverlayFitMode::None,
|
fit_mode: OverlayFitMode::None,
|
||||||
|
position_mode: OverlayPositionMode::Window,
|
||||||
hoverable: false,
|
hoverable: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +100,11 @@ impl Overlay {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_position_mode(mut self, position_mode: OverlayPositionMode) -> Self {
|
||||||
|
self.position_mode = position_mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_hoverable(mut self, hoverable: bool) -> Self {
|
pub fn with_hoverable(mut self, hoverable: bool) -> Self {
|
||||||
self.hoverable = hoverable;
|
self.hoverable = hoverable;
|
||||||
self
|
self
|
||||||
|
@ -123,8 +136,20 @@ impl Element for Overlay {
|
||||||
size: &mut Self::LayoutState,
|
size: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
|
let (anchor_position, mut bounds) = match self.position_mode {
|
||||||
|
OverlayPositionMode::Window => {
|
||||||
let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
|
let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
|
||||||
let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size);
|
let bounds = self.anchor_corner.get_bounds(anchor_position, *size);
|
||||||
|
(anchor_position, bounds)
|
||||||
|
}
|
||||||
|
OverlayPositionMode::Local => {
|
||||||
|
let anchor_position = self.anchor_position.unwrap_or_default();
|
||||||
|
let bounds = self
|
||||||
|
.anchor_corner
|
||||||
|
.get_bounds(bounds.origin() + anchor_position, *size);
|
||||||
|
(anchor_position, bounds)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match self.fit_mode {
|
match self.fit_mode {
|
||||||
OverlayFitMode::SnapToWindow => {
|
OverlayFitMode::SnapToWindow => {
|
||||||
|
|
|
@ -385,6 +385,8 @@ impl TerminalBuilder {
|
||||||
breadcrumb_text: String::new(),
|
breadcrumb_text: String::new(),
|
||||||
scroll_px: 0.,
|
scroll_px: 0.,
|
||||||
last_mouse_position: None,
|
last_mouse_position: None,
|
||||||
|
next_link_id: 0,
|
||||||
|
selection_phase: SelectionPhase::Ended,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(TerminalBuilder {
|
Ok(TerminalBuilder {
|
||||||
|
@ -471,7 +473,7 @@ pub struct TerminalContent {
|
||||||
cursor: RenderableCursor,
|
cursor: RenderableCursor,
|
||||||
cursor_char: char,
|
cursor_char: char,
|
||||||
size: TerminalSize,
|
size: TerminalSize,
|
||||||
last_hovered_hyperlink: Option<(String, RangeInclusive<Point>)>,
|
last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TerminalContent {
|
impl Default for TerminalContent {
|
||||||
|
@ -493,6 +495,12 @@ impl Default for TerminalContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub enum SelectionPhase {
|
||||||
|
Selecting,
|
||||||
|
Ended,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Terminal {
|
pub struct Terminal {
|
||||||
pty_tx: Notifier,
|
pty_tx: Notifier,
|
||||||
term: Arc<FairMutex<Term<ZedListener>>>,
|
term: Arc<FairMutex<Term<ZedListener>>>,
|
||||||
|
@ -511,6 +519,8 @@ pub struct Terminal {
|
||||||
shell_fd: u32,
|
shell_fd: u32,
|
||||||
foreground_process_info: Option<LocalProcessInfo>,
|
foreground_process_info: Option<LocalProcessInfo>,
|
||||||
scroll_px: f32,
|
scroll_px: f32,
|
||||||
|
next_link_id: usize,
|
||||||
|
selection_phase: SelectionPhase,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Terminal {
|
impl Terminal {
|
||||||
|
@ -654,7 +664,7 @@ impl Terminal {
|
||||||
}
|
}
|
||||||
InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
|
InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
|
||||||
InternalEvent::Hyperlink(position, open) => {
|
InternalEvent::Hyperlink(position, open) => {
|
||||||
self.last_content.last_hovered_hyperlink = None;
|
let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
|
||||||
|
|
||||||
let point = grid_point(
|
let point = grid_point(
|
||||||
*position,
|
*position,
|
||||||
|
@ -668,13 +678,37 @@ impl Terminal {
|
||||||
if *open {
|
if *open {
|
||||||
open_uri(&url).log_err();
|
open_uri(&url).log_err();
|
||||||
} else {
|
} else {
|
||||||
self.last_content.last_hovered_hyperlink = Some((url, url_match));
|
self.update_hyperlink(prev_hyperlink, url, url_match);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_hyperlink(
|
||||||
|
&mut self,
|
||||||
|
prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
|
||||||
|
url: String,
|
||||||
|
url_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));
|
||||||
|
} else {
|
||||||
|
self.last_content.last_hovered_hyperlink =
|
||||||
|
Some((url, url_match, self.next_link_id()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_link_id(&mut self) -> usize {
|
||||||
|
let res = self.next_link_id;
|
||||||
|
self.next_link_id = self.next_link_id.wrapping_add(1);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
pub fn last_content(&self) -> &TerminalContent {
|
pub fn last_content(&self) -> &TerminalContent {
|
||||||
&self.last_content
|
&self.last_content
|
||||||
}
|
}
|
||||||
|
@ -846,7 +880,8 @@ impl Terminal {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
|
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
|
||||||
self.last_content.last_hovered_hyperlink = None;
|
let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
|
||||||
|
|
||||||
let position = e.position.sub(origin);
|
let position = e.position.sub(origin);
|
||||||
self.last_mouse_position = Some(position);
|
self.last_mouse_position = Some(position);
|
||||||
if self.mouse_mode(e.shift) {
|
if self.mouse_mode(e.shift) {
|
||||||
|
@ -862,19 +897,26 @@ impl Terminal {
|
||||||
self.pty_tx.notify(bytes);
|
self.pty_tx.notify(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if e.cmd {
|
} else {
|
||||||
self.fill_hyperlink(Some(position));
|
self.fill_hyperlink(Some(position), prev_hyperlink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_hyperlink(&mut self, position: Option<Vector2F>) {
|
fn fill_hyperlink(
|
||||||
if let Some(position) = position {
|
&mut self,
|
||||||
|
position: Option<Vector2F>,
|
||||||
|
prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
|
||||||
|
) {
|
||||||
|
if self.selection_phase == SelectionPhase::Selecting {
|
||||||
|
self.last_content.last_hovered_hyperlink = None;
|
||||||
|
} else if let Some(position) = position {
|
||||||
let content_index = content_index_for_mouse(position, &self.last_content);
|
let content_index = content_index_for_mouse(position, &self.last_content);
|
||||||
let link = self.last_content.cells[content_index].hyperlink();
|
let link = self.last_content.cells[content_index].hyperlink();
|
||||||
if link.is_some() {
|
if link.is_some() {
|
||||||
let mut min_index = content_index;
|
let mut min_index = content_index;
|
||||||
loop {
|
loop {
|
||||||
if self.last_content.cells[min_index - 1].hyperlink() == link {
|
if min_index >= 1 && self.last_content.cells[min_index - 1].hyperlink() == link
|
||||||
|
{
|
||||||
min_index = min_index - 1;
|
min_index = min_index - 1;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
@ -882,21 +924,24 @@ impl Terminal {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut max_index = content_index;
|
let mut max_index = content_index;
|
||||||
|
let len = self.last_content.cells.len();
|
||||||
loop {
|
loop {
|
||||||
if self.last_content.cells[max_index + 1].hyperlink() == link {
|
if max_index < len - 1
|
||||||
|
&& self.last_content.cells[max_index + 1].hyperlink() == link
|
||||||
|
{
|
||||||
max_index = max_index + 1;
|
max_index = max_index + 1;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_content.last_hovered_hyperlink = link.map(|link| {
|
if let Some(link) = link {
|
||||||
(
|
let url = link.uri().to_owned();
|
||||||
link.uri().to_owned(),
|
let url_match = self.last_content.cells[min_index].point
|
||||||
self.last_content.cells[min_index].point
|
..=self.last_content.cells[max_index].point;
|
||||||
..=self.last_content.cells[max_index].point,
|
|
||||||
)
|
self.update_hyperlink(prev_hyperlink, url, url_match);
|
||||||
});
|
};
|
||||||
} else {
|
} else {
|
||||||
self.events
|
self.events
|
||||||
.push_back(InternalEvent::Hyperlink(position, false));
|
.push_back(InternalEvent::Hyperlink(position, false));
|
||||||
|
@ -909,6 +954,7 @@ impl Terminal {
|
||||||
self.last_mouse_position = Some(position);
|
self.last_mouse_position = Some(position);
|
||||||
|
|
||||||
if !self.mouse_mode(e.shift) {
|
if !self.mouse_mode(e.shift) {
|
||||||
|
self.selection_phase = SelectionPhase::Selecting;
|
||||||
// Alacritty has the same ordering, of first updating the selection
|
// Alacritty has the same ordering, of first updating the selection
|
||||||
// then scrolling 15ms later
|
// then scrolling 15ms later
|
||||||
self.events
|
self.events
|
||||||
|
@ -969,7 +1015,9 @@ impl Terminal {
|
||||||
pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
|
pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
|
||||||
let position = e.position.sub(origin);
|
let position = e.position.sub(origin);
|
||||||
if !self.mouse_mode(e.shift) {
|
if !self.mouse_mode(e.shift) {
|
||||||
if e.cmd {
|
if self.last_content.last_hovered_hyperlink.is_some()
|
||||||
|
&& self.last_content.selection.is_none()
|
||||||
|
{
|
||||||
let mouse_cell_index = content_index_for_mouse(position, &self.last_content);
|
let mouse_cell_index = content_index_for_mouse(position, &self.last_content);
|
||||||
if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
|
if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
|
||||||
open_uri(link.uri()).log_err();
|
open_uri(link.uri()).log_err();
|
||||||
|
@ -1021,6 +1069,7 @@ impl Terminal {
|
||||||
// so let's do that here
|
// so let's do that here
|
||||||
self.copy();
|
self.copy();
|
||||||
}
|
}
|
||||||
|
self.selection_phase = SelectionPhase::Ended;
|
||||||
self.last_mouse = None;
|
self.last_mouse = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1061,10 +1110,10 @@ impl Terminal {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool {
|
pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool {
|
||||||
self.last_content.last_hovered_hyperlink = None;
|
let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
|
||||||
|
|
||||||
if cmd {
|
if cmd {
|
||||||
self.fill_hyperlink(self.last_mouse_position);
|
self.fill_hyperlink(self.last_mouse_position, prev_hyperlink);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
|
@ -7,7 +7,7 @@ use alacritty_terminal::{
|
||||||
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
|
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
elements::{Overlay, Tooltip},
|
elements::{Empty, Overlay},
|
||||||
fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight},
|
fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight},
|
||||||
geometry::{
|
geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
|
@ -15,7 +15,7 @@ use gpui::{
|
||||||
},
|
},
|
||||||
serde_json::json,
|
serde_json::json,
|
||||||
text_layout::{Line, RunStyle},
|
text_layout::{Line, RunStyle},
|
||||||
Axis, Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext,
|
Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext,
|
||||||
ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint,
|
ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint,
|
||||||
TextLayoutCache, WeakModelHandle, WeakViewHandle,
|
TextLayoutCache, WeakModelHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
|
@ -324,7 +324,6 @@ impl TerminalElement {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if indexed.cell.hyperlink().is_some() {
|
if indexed.cell.hyperlink().is_some() {
|
||||||
underline.squiggly = true;
|
|
||||||
if underline.thickness == OrderedFloat(0.) {
|
if underline.thickness == OrderedFloat(0.) {
|
||||||
underline.thickness = OrderedFloat(1.);
|
underline.thickness = OrderedFloat(1.);
|
||||||
}
|
}
|
||||||
|
@ -594,51 +593,34 @@ impl Element for TerminalElement {
|
||||||
};
|
};
|
||||||
let terminal_handle = self.terminal.upgrade(cx).unwrap();
|
let terminal_handle = self.terminal.upgrade(cx).unwrap();
|
||||||
|
|
||||||
let (last_hovered_hyperlink, last_mouse) =
|
let last_hovered_hyperlink = terminal_handle.update(cx.app, |terminal, cx| {
|
||||||
terminal_handle.update(cx.app, |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_hyperlink.clone(),
|
|
||||||
terminal.last_mouse_position,
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let view_handle = self.view.clone();
|
let view_handle = self.view.clone();
|
||||||
let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _)| {
|
let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _, id)| {
|
||||||
last_mouse.and_then(|last_mouse| {
|
// last_mouse.and_then(|_last_mouse| {
|
||||||
view_handle.upgrade(cx).map(|handle| {
|
view_handle.upgrade(cx).map(|handle| {
|
||||||
let mut tooltip = cx.render(&handle, |_, cx| {
|
let mut tooltip = cx.render(&handle, |_, cx| {
|
||||||
// TODO: Use the correct dynamic line height
|
|
||||||
// let mut collapsed_tooltip = Tooltip::render_tooltip(
|
|
||||||
// uri.clone(),
|
|
||||||
// tooltip_style.clone(),
|
|
||||||
// None,
|
|
||||||
// false,
|
|
||||||
// )
|
|
||||||
// .boxed();
|
|
||||||
|
|
||||||
Overlay::new(
|
Overlay::new(
|
||||||
Tooltip::render_tooltip(uri, tooltip_style, None, false)
|
Empty::new()
|
||||||
|
.contained()
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(text_style.line_height(cx.font_cache()))
|
.with_width(dimensions.width())
|
||||||
// .dynamically(move |constraint, cx| {
|
.with_height(dimensions.height())
|
||||||
// SizeConstraint::strict_along(
|
.with_tooltip::<TerminalElement, _>(id, uri, None, tooltip_style, cx)
|
||||||
// Axis::Vertical,
|
|
||||||
// collapsed_tooltip.layout(constraint, cx).y(),
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_fit_mode(gpui::elements::OverlayFitMode::SwitchAnchor)
|
.with_position_mode(gpui::elements::OverlayPositionMode::Local)
|
||||||
.with_anchor_position(last_mouse)
|
|
||||||
.boxed()
|
.boxed()
|
||||||
});
|
});
|
||||||
|
|
||||||
tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
|
tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
|
||||||
tooltip
|
tooltip
|
||||||
})
|
})
|
||||||
})
|
// })
|
||||||
});
|
});
|
||||||
|
|
||||||
let TerminalContent {
|
let TerminalContent {
|
||||||
|
@ -672,7 +654,7 @@ impl Element for TerminalElement {
|
||||||
self.modal,
|
self.modal,
|
||||||
last_hovered_hyperlink
|
last_hovered_hyperlink
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|(_, range)| (link_style, range)),
|
.map(|(_, range, _)| (link_style, range)),
|
||||||
);
|
);
|
||||||
|
|
||||||
//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
|
||||||
|
@ -822,12 +804,7 @@ impl Element for TerminalElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(element) = &mut layout.hyperlink_tooltip {
|
if let Some(element) = &mut layout.hyperlink_tooltip {
|
||||||
element.paint(
|
element.paint(origin, visible_bounds, cx)
|
||||||
visible_bounds.lower_left()
|
|
||||||
- vec2f(-layout.size.cell_width, layout.size.line_height),
|
|
||||||
visible_bounds,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue