mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-09 05:37:15 +03:00
Merge pull request #1637 from zed-industries/terminal-hyperlinks
Tracking PR for Terminal hyperlinks
This commit is contained in:
commit
98aad9a727
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -5482,11 +5482,13 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"mio-extras",
|
"mio-extras",
|
||||||
"ordered-float",
|
"ordered-float",
|
||||||
"procinfo",
|
"procinfo",
|
||||||
"project",
|
"project",
|
||||||
|
"rand 0.8.5",
|
||||||
"settings",
|
"settings",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
@ -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 = self.anchor_position.unwrap_or_else(|| bounds.origin());
|
let (anchor_position, mut bounds) = match self.position_mode {
|
||||||
let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size);
|
OverlayPositionMode::Window => {
|
||||||
|
let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
|
||||||
|
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 => {
|
||||||
|
@ -36,10 +36,10 @@ struct TooltipState {
|
|||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
pub struct TooltipStyle {
|
pub struct TooltipStyle {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
text: TextStyle,
|
pub text: TextStyle,
|
||||||
keystroke: KeystrokeStyle,
|
keystroke: KeystrokeStyle,
|
||||||
max_text_width: f32,
|
pub max_text_width: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
@ -126,7 +126,7 @@ impl Tooltip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tooltip(
|
pub fn render_tooltip(
|
||||||
text: String,
|
text: String,
|
||||||
style: TooltipStyle,
|
style: TooltipStyle,
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
|
@ -29,6 +29,8 @@ shellexpand = "2.1.0"
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
@ -36,3 +38,4 @@ gpui = { path = "../gpui", features = ["test-support"] }
|
|||||||
client = { path = "../client", features = ["test-support"]}
|
client = { path = "../client", features = ["test-support"]}
|
||||||
project = { path = "../project", features = ["test-support"]}
|
project = { path = "../project", features = ["test-support"]}
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
@ -202,7 +202,7 @@ pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal::
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
|
pub fn grid_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
|
||||||
let col = pos.x() / cur_size.cell_width;
|
let col = pos.x() / cur_size.cell_width;
|
||||||
let col = min(GridCol(col as usize), cur_size.last_column());
|
let col = min(GridCol(col as usize), cur_size.last_column());
|
||||||
let line = pos.y() / cur_size.line_height;
|
let line = pos.y() / cur_size.line_height;
|
||||||
@ -295,7 +295,7 @@ fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::mappings::mouse::mouse_point;
|
use crate::mappings::mouse::grid_point;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mouse_to_selection() {
|
fn test_mouse_to_selection() {
|
||||||
@ -317,7 +317,7 @@ mod test {
|
|||||||
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
|
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
|
||||||
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
|
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
|
||||||
let mouse_pos = mouse_pos - origin;
|
let mouse_pos = mouse_pos - origin;
|
||||||
let point = mouse_point(mouse_pos, cur_size, 0);
|
let point = grid_point(mouse_pos, cur_size, 0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
point,
|
point,
|
||||||
alacritty_terminal::index::Point::new(
|
alacritty_terminal::index::Point::new(
|
||||||
|
@ -29,18 +29,22 @@ use futures::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use mappings::mouse::{
|
use mappings::mouse::{
|
||||||
alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
|
alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
|
||||||
};
|
};
|
||||||
|
|
||||||
use procinfo::LocalProcessInfo;
|
use procinfo::LocalProcessInfo;
|
||||||
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
|
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
cmp::min,
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
|
io,
|
||||||
ops::{Deref, RangeInclusive, Sub},
|
ops::{Deref, RangeInclusive, Sub},
|
||||||
os::unix::prelude::AsRawFd,
|
os::unix::{prelude::AsRawFd, process::CommandExt},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
process::Command,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
@ -59,6 +63,7 @@ use crate::mappings::{
|
|||||||
colors::{get_color_at_index, to_alac_rgb},
|
colors::{get_color_at_index, to_alac_rgb},
|
||||||
keys::to_esc_str,
|
keys::to_esc_str,
|
||||||
};
|
};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
///Initialize and register all of our action handlers
|
///Initialize and register all of our action handlers
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
@ -70,12 +75,18 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||||||
///Scroll multiplier that is set to 3 by default. This will be removed when I
|
///Scroll multiplier that is set to 3 by default. This will be removed when I
|
||||||
///Implement scroll bars.
|
///Implement scroll bars.
|
||||||
const SCROLL_MULTIPLIER: f32 = 4.;
|
const SCROLL_MULTIPLIER: f32 = 4.;
|
||||||
// const MAX_SEARCH_LINES: usize = 100;
|
const MAX_SEARCH_LINES: usize = 100;
|
||||||
const DEBUG_TERMINAL_WIDTH: f32 = 500.;
|
const DEBUG_TERMINAL_WIDTH: f32 = 500.;
|
||||||
const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
|
const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
|
||||||
const DEBUG_CELL_WIDTH: f32 = 5.;
|
const DEBUG_CELL_WIDTH: f32 = 5.;
|
||||||
const DEBUG_LINE_HEIGHT: f32 = 5.;
|
const DEBUG_LINE_HEIGHT: f32 = 5.;
|
||||||
|
|
||||||
|
// Regex Copied from alacritty's ui_config.rs
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
///Upward flowing events, for changing the title and such
|
///Upward flowing events, for changing the title and such
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
@ -98,6 +109,8 @@ enum InternalEvent {
|
|||||||
ScrollToPoint(Point),
|
ScrollToPoint(Point),
|
||||||
SetSelection(Option<(Selection, Point)>),
|
SetSelection(Option<(Selection, Point)>),
|
||||||
UpdateSelection(Vector2F),
|
UpdateSelection(Vector2F),
|
||||||
|
// Adjusted mouse position, should open
|
||||||
|
Hyperlink(Vector2F, bool),
|
||||||
Copy,
|
Copy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +280,6 @@ impl TerminalBuilder {
|
|||||||
working_directory: Option<PathBuf>,
|
working_directory: Option<PathBuf>,
|
||||||
shell: Option<Shell>,
|
shell: Option<Shell>,
|
||||||
env: Option<HashMap<String, String>>,
|
env: Option<HashMap<String, String>>,
|
||||||
initial_size: TerminalSize,
|
|
||||||
blink_settings: Option<TerminalBlink>,
|
blink_settings: Option<TerminalBlink>,
|
||||||
alternate_scroll: &AlternateScroll,
|
alternate_scroll: &AlternateScroll,
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
@ -307,7 +319,11 @@ impl TerminalBuilder {
|
|||||||
//TODO: Remove with a bounded sender which can be dispatched on &self
|
//TODO: Remove with a bounded sender which can be dispatched on &self
|
||||||
let (events_tx, events_rx) = unbounded();
|
let (events_tx, events_rx) = unbounded();
|
||||||
//Set up the terminal...
|
//Set up the terminal...
|
||||||
let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
|
let mut term = Term::new(
|
||||||
|
&config,
|
||||||
|
&TerminalSize::default(),
|
||||||
|
ZedListener(events_tx.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
//Start off blinking if we need to
|
//Start off blinking if we need to
|
||||||
if let Some(TerminalBlink::On) = blink_settings {
|
if let Some(TerminalBlink::On) = blink_settings {
|
||||||
@ -322,7 +338,11 @@ impl TerminalBuilder {
|
|||||||
let term = Arc::new(FairMutex::new(term));
|
let term = Arc::new(FairMutex::new(term));
|
||||||
|
|
||||||
//Setup the pty...
|
//Setup the pty...
|
||||||
let pty = match tty::new(&pty_config, initial_size.into(), window_id as u64) {
|
let pty = match tty::new(
|
||||||
|
&pty_config,
|
||||||
|
TerminalSize::default().into(),
|
||||||
|
window_id as u64,
|
||||||
|
) {
|
||||||
Ok(pty) => pty,
|
Ok(pty) => pty,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
bail!(TerminalError {
|
bail!(TerminalError {
|
||||||
@ -354,7 +374,6 @@ impl TerminalBuilder {
|
|||||||
term,
|
term,
|
||||||
events: VecDeque::with_capacity(10), //Should never get this high.
|
events: VecDeque::with_capacity(10), //Should never get this high.
|
||||||
last_content: Default::default(),
|
last_content: Default::default(),
|
||||||
cur_size: initial_size,
|
|
||||||
last_mouse: None,
|
last_mouse: None,
|
||||||
matches: Vec::new(),
|
matches: Vec::new(),
|
||||||
last_synced: Instant::now(),
|
last_synced: Instant::now(),
|
||||||
@ -365,6 +384,9 @@ impl TerminalBuilder {
|
|||||||
foreground_process_info: None,
|
foreground_process_info: None,
|
||||||
breadcrumb_text: String::new(),
|
breadcrumb_text: String::new(),
|
||||||
scroll_px: 0.,
|
scroll_px: 0.,
|
||||||
|
last_mouse_position: None,
|
||||||
|
next_link_id: 0,
|
||||||
|
selection_phase: SelectionPhase::Ended,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(TerminalBuilder {
|
Ok(TerminalBuilder {
|
||||||
@ -450,6 +472,8 @@ pub struct TerminalContent {
|
|||||||
selection: Option<SelectionRange>,
|
selection: Option<SelectionRange>,
|
||||||
cursor: RenderableCursor,
|
cursor: RenderableCursor,
|
||||||
cursor_char: char,
|
cursor_char: char,
|
||||||
|
size: TerminalSize,
|
||||||
|
last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TerminalContent {
|
impl Default for TerminalContent {
|
||||||
@ -465,17 +489,27 @@ impl Default for TerminalContent {
|
|||||||
point: Point::new(Line(0), Column(0)),
|
point: Point::new(Line(0), Column(0)),
|
||||||
},
|
},
|
||||||
cursor_char: Default::default(),
|
cursor_char: Default::default(),
|
||||||
|
size: Default::default(),
|
||||||
|
last_hovered_hyperlink: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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>>>,
|
||||||
events: VecDeque<InternalEvent>,
|
events: VecDeque<InternalEvent>,
|
||||||
|
/// 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
|
||||||
|
last_mouse_position: Option<Vector2F>,
|
||||||
pub matches: Vec<RangeInclusive<Point>>,
|
pub matches: Vec<RangeInclusive<Point>>,
|
||||||
cur_size: TerminalSize,
|
|
||||||
last_content: TerminalContent,
|
last_content: TerminalContent,
|
||||||
last_synced: Instant,
|
last_synced: Instant,
|
||||||
sync_task: Option<Task<()>>,
|
sync_task: Option<Task<()>>,
|
||||||
@ -485,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 {
|
||||||
@ -508,7 +544,7 @@ impl Terminal {
|
|||||||
)),
|
)),
|
||||||
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
|
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
|
||||||
AlacTermEvent::TextAreaSizeRequest(format) => {
|
AlacTermEvent::TextAreaSizeRequest(format) => {
|
||||||
self.write_to_pty(format(self.cur_size.into()))
|
self.write_to_pty(format(self.last_content.size.into()))
|
||||||
}
|
}
|
||||||
AlacTermEvent::CursorBlinkingChange => {
|
AlacTermEvent::CursorBlinkingChange => {
|
||||||
cx.emit(Event::BlinkChanged);
|
cx.emit(Event::BlinkChanged);
|
||||||
@ -577,7 +613,7 @@ impl Terminal {
|
|||||||
new_size.height = f32::max(new_size.line_height, new_size.height);
|
new_size.height = f32::max(new_size.line_height, new_size.height);
|
||||||
new_size.width = f32::max(new_size.cell_width, new_size.width);
|
new_size.width = f32::max(new_size.cell_width, new_size.width);
|
||||||
|
|
||||||
self.cur_size = new_size.clone();
|
self.last_content.size = new_size.clone();
|
||||||
|
|
||||||
self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
|
self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
|
||||||
|
|
||||||
@ -606,8 +642,12 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
InternalEvent::UpdateSelection(position) => {
|
InternalEvent::UpdateSelection(position) => {
|
||||||
if let Some(mut selection) = term.selection.take() {
|
if let Some(mut selection) = term.selection.take() {
|
||||||
let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
|
let point = grid_point(
|
||||||
let side = mouse_side(*position, self.cur_size);
|
*position,
|
||||||
|
self.last_content.size,
|
||||||
|
term.grid().display_offset(),
|
||||||
|
);
|
||||||
|
let side = mouse_side(*position, self.last_content.size);
|
||||||
|
|
||||||
selection.update(point, side);
|
selection.update(point, side);
|
||||||
term.selection = Some(selection);
|
term.selection = Some(selection);
|
||||||
@ -623,9 +663,52 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
|
InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
|
||||||
|
InternalEvent::Hyperlink(position, open) => {
|
||||||
|
let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
|
||||||
|
|
||||||
|
let point = grid_point(
|
||||||
|
*position,
|
||||||
|
self.last_content.size,
|
||||||
|
term.grid().display_offset(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) {
|
||||||
|
let url = term.bounds_to_string(*url_match.start(), *url_match.end());
|
||||||
|
|
||||||
|
if *open {
|
||||||
|
open_uri(&url).log_err();
|
||||||
|
} else {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@ -730,11 +813,11 @@ impl Terminal {
|
|||||||
self.process_terminal_event(&e, &mut terminal, cx)
|
self.process_terminal_event(&e, &mut terminal, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_content = Self::make_content(&terminal);
|
self.last_content = Self::make_content(&terminal, &self.last_content);
|
||||||
self.last_synced = Instant::now();
|
self.last_synced = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_content(term: &Term<ZedListener>) -> TerminalContent {
|
fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
|
||||||
let content = term.renderable_content();
|
let content = term.renderable_content();
|
||||||
TerminalContent {
|
TerminalContent {
|
||||||
cells: content
|
cells: content
|
||||||
@ -757,6 +840,8 @@ impl Terminal {
|
|||||||
selection: content.selection,
|
selection: content.selection,
|
||||||
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,
|
||||||
|
last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -766,7 +851,8 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focus_out(&self) {
|
pub fn focus_out(&mut self) {
|
||||||
|
self.last_mouse_position = None;
|
||||||
if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
|
if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
|
||||||
self.write_to_pty("\x1b[O".to_string());
|
self.write_to_pty("\x1b[O".to_string());
|
||||||
}
|
}
|
||||||
@ -794,22 +880,81 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
|
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
|
||||||
|
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);
|
||||||
|
if self.mouse_mode(e.shift) {
|
||||||
|
let point = grid_point(
|
||||||
|
position,
|
||||||
|
self.last_content.size,
|
||||||
|
self.last_content.display_offset,
|
||||||
|
);
|
||||||
|
let side = mouse_side(position, self.last_content.size);
|
||||||
|
|
||||||
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
|
if self.mouse_changed(point, side) {
|
||||||
let side = mouse_side(position, self.cur_size);
|
if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
|
||||||
|
self.pty_tx.notify(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.fill_hyperlink(Some(position), prev_hyperlink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
|
fn fill_hyperlink(
|
||||||
if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
|
&mut self,
|
||||||
self.pty_tx.notify(bytes);
|
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 link = self.last_content.cells[content_index].hyperlink();
|
||||||
|
if link.is_some() {
|
||||||
|
let mut min_index = content_index;
|
||||||
|
loop {
|
||||||
|
if min_index >= 1 && self.last_content.cells[min_index - 1].hyperlink() == link
|
||||||
|
{
|
||||||
|
min_index = min_index - 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut max_index = content_index;
|
||||||
|
let len = self.last_content.cells.len();
|
||||||
|
loop {
|
||||||
|
if max_index < len - 1
|
||||||
|
&& self.last_content.cells[max_index + 1].hyperlink() == link
|
||||||
|
{
|
||||||
|
max_index = max_index + 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(link) = link {
|
||||||
|
let url = link.uri().to_owned();
|
||||||
|
let url_match = self.last_content.cells[min_index].point
|
||||||
|
..=self.last_content.cells[max_index].point;
|
||||||
|
|
||||||
|
self.update_hyperlink(prev_hyperlink, url, url_match);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.events
|
||||||
|
.push_back(InternalEvent::Hyperlink(position, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
|
pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
|
||||||
let position = e.position.sub(origin);
|
let position = e.position.sub(origin);
|
||||||
|
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
|
||||||
@ -822,7 +967,7 @@ impl Terminal {
|
|||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
|
let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
|
||||||
|
|
||||||
self.events
|
self.events
|
||||||
.push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
|
.push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
|
||||||
@ -834,8 +979,8 @@ impl Terminal {
|
|||||||
|
|
||||||
fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
|
fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
|
||||||
//TODO: Why do these need to be doubled? Probably the same problem that the IME has
|
//TODO: Why do these need to be doubled? Probably the same problem that the IME has
|
||||||
let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
|
let top = e.region.origin_y() + (self.last_content.size.line_height * 2.);
|
||||||
let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
|
let bottom = e.region.lower_left().y() - (self.last_content.size.line_height * 2.);
|
||||||
let scroll_delta = if e.position.y() < top {
|
let scroll_delta = if e.position.y() < top {
|
||||||
(top - e.position.y()).powf(1.1)
|
(top - e.position.y()).powf(1.1)
|
||||||
} else if e.position.y() > bottom {
|
} else if e.position.y() > bottom {
|
||||||
@ -848,8 +993,12 @@ impl Terminal {
|
|||||||
|
|
||||||
pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
|
pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
|
||||||
let position = e.position.sub(origin);
|
let position = e.position.sub(origin);
|
||||||
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
|
let point = grid_point(
|
||||||
let side = mouse_side(position, self.cur_size);
|
position,
|
||||||
|
self.last_content.size,
|
||||||
|
self.last_content.display_offset,
|
||||||
|
);
|
||||||
|
let side = mouse_side(position, self.last_content.size);
|
||||||
|
|
||||||
if self.mouse_mode(e.shift) {
|
if self.mouse_mode(e.shift) {
|
||||||
if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
|
if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
|
||||||
@ -865,25 +1014,42 @@ 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) {
|
||||||
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
|
//Hyperlinks
|
||||||
let side = mouse_side(position, self.cur_size);
|
{
|
||||||
|
let mouse_cell_index = content_index_for_mouse(position, &self.last_content);
|
||||||
|
if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
|
||||||
|
open_uri(link.uri()).log_err();
|
||||||
|
} else {
|
||||||
|
self.events
|
||||||
|
.push_back(InternalEvent::Hyperlink(position, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let selection_type = match e.click_count {
|
// Selections
|
||||||
0 => return, //This is a release
|
{
|
||||||
1 => Some(SelectionType::Simple),
|
let point = grid_point(
|
||||||
2 => Some(SelectionType::Semantic),
|
position,
|
||||||
3 => Some(SelectionType::Lines),
|
self.last_content.size,
|
||||||
_ => None,
|
self.last_content.display_offset,
|
||||||
};
|
);
|
||||||
|
let side = mouse_side(position, self.last_content.size);
|
||||||
|
|
||||||
let selection =
|
let selection_type = match e.click_count {
|
||||||
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
|
0 => return, //This is a release
|
||||||
|
1 => Some(SelectionType::Simple),
|
||||||
|
2 => Some(SelectionType::Semantic),
|
||||||
|
3 => Some(SelectionType::Lines),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(sel) = selection {
|
let selection = selection_type
|
||||||
self.events
|
.map(|selection_type| Selection::new(selection_type, point, side));
|
||||||
.push_back(InternalEvent::SetSelection(Some((sel, point))));
|
|
||||||
|
if let Some(sel) = selection {
|
||||||
|
self.events
|
||||||
|
.push_back(InternalEvent::SetSelection(Some((sel, point))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -891,7 +1057,11 @@ impl Terminal {
|
|||||||
pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
|
pub fn mouse_up(&mut self, e: &UpRegionEvent, 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) {
|
||||||
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
|
let point = grid_point(
|
||||||
|
position,
|
||||||
|
self.last_content.size,
|
||||||
|
self.last_content.display_offset,
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
|
if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
|
||||||
self.pty_tx.notify(bytes);
|
self.pty_tx.notify(bytes);
|
||||||
@ -901,6 +1071,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -910,9 +1081,9 @@ impl Terminal {
|
|||||||
|
|
||||||
if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
|
if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
|
||||||
if mouse_mode {
|
if mouse_mode {
|
||||||
let point = mouse_point(
|
let point = grid_point(
|
||||||
e.position.sub(origin),
|
e.position.sub(origin),
|
||||||
self.cur_size,
|
self.last_content.size,
|
||||||
self.last_content.display_offset,
|
self.last_content.display_offset,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -940,6 +1111,17 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool {
|
||||||
|
let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
|
||||||
|
|
||||||
|
if cmd {
|
||||||
|
self.fill_hyperlink(self.last_mouse_position, prev_hyperlink);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn determine_scroll_lines(
|
fn determine_scroll_lines(
|
||||||
&mut self,
|
&mut self,
|
||||||
e: &ScrollWheelRegionEvent,
|
e: &ScrollWheelRegionEvent,
|
||||||
@ -955,20 +1137,22 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
/* Calculate the appropriate scroll lines */
|
/* Calculate the appropriate scroll lines */
|
||||||
Some(gpui::TouchPhase::Moved) => {
|
Some(gpui::TouchPhase::Moved) => {
|
||||||
let old_offset = (self.scroll_px / self.cur_size.line_height) as i32;
|
let old_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
|
||||||
|
|
||||||
self.scroll_px += e.delta.y() * scroll_multiplier;
|
self.scroll_px += e.delta.y() * scroll_multiplier;
|
||||||
|
|
||||||
let new_offset = (self.scroll_px / self.cur_size.line_height) as i32;
|
let new_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
|
||||||
|
|
||||||
// Whenever we hit the edges, reset our stored scroll to 0
|
// Whenever we hit the edges, reset our stored scroll to 0
|
||||||
// so we can respond to changes in direction quickly
|
// so we can respond to changes in direction quickly
|
||||||
self.scroll_px %= self.cur_size.height;
|
self.scroll_px %= self.last_content.size.height;
|
||||||
|
|
||||||
Some(new_offset - old_offset)
|
Some(new_offset - old_offset)
|
||||||
}
|
}
|
||||||
/* Fall back to delta / line_height */
|
/* Fall back to delta / line_height */
|
||||||
None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32),
|
None => Some(
|
||||||
|
((e.delta.y() * scroll_multiplier) / self.last_content.size.line_height) as i32,
|
||||||
|
),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1011,30 +1195,36 @@ impl Entity for Terminal {
|
|||||||
type Event = Event;
|
type Event = Event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Based on alacritty/src/display/hint.rs > regex_match_at
|
||||||
|
/// Retrieve the match, if the specified point is inside the content matching the regex.
|
||||||
|
fn regex_match_at<T>(term: &Term<T>, point: Point, regex: &RegexSearch) -> Option<Match> {
|
||||||
|
visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copied from alacritty/src/display/hint.rs:
|
||||||
|
/// Iterate over all visible regex matches.
|
||||||
|
pub fn visible_regex_match_iter<'a, T>(
|
||||||
|
term: &'a Term<T>,
|
||||||
|
regex: &'a RegexSearch,
|
||||||
|
) -> impl Iterator<Item = Match> + 'a {
|
||||||
|
let viewport_start = Line(-(term.grid().display_offset() as i32));
|
||||||
|
let viewport_end = viewport_start + term.bottommost_line();
|
||||||
|
let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
|
||||||
|
let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
|
||||||
|
start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
|
||||||
|
end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
|
||||||
|
|
||||||
|
RegexIter::new(start, end, AlacDirection::Right, term, regex)
|
||||||
|
.skip_while(move |rm| rm.end().line < viewport_start)
|
||||||
|
.take_while(move |rm| rm.start().line <= viewport_end)
|
||||||
|
}
|
||||||
|
|
||||||
fn make_selection(range: &RangeInclusive<Point>) -> Selection {
|
fn make_selection(range: &RangeInclusive<Point>) -> Selection {
|
||||||
let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
|
let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
|
||||||
selection.update(*range.end(), AlacDirection::Right);
|
selection.update(*range.end(), AlacDirection::Right);
|
||||||
selection
|
selection
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
|
|
||||||
/// Iterate over all visible regex matches.
|
|
||||||
// fn visible_search_matches<'a, T>(
|
|
||||||
// term: &'a Term<T>,
|
|
||||||
// regex: &'a RegexSearch,
|
|
||||||
// ) -> impl Iterator<Item = Match> + 'a {
|
|
||||||
// let viewport_start = Line(-(term.grid().display_offset() as i32));
|
|
||||||
// let viewport_end = viewport_start + term.bottommost_line();
|
|
||||||
// let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
|
|
||||||
// let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
|
|
||||||
// start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
|
|
||||||
// end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
|
|
||||||
|
|
||||||
// RegexIter::new(start, end, AlacDirection::Right, term, regex)
|
|
||||||
// .skip_while(move |rm| rm.end().line < viewport_start)
|
|
||||||
// .take_while(move |rm| rm.start().line <= viewport_end)
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn all_search_matches<'a, T>(
|
fn all_search_matches<'a, T>(
|
||||||
term: &'a Term<T>,
|
term: &'a Term<T>,
|
||||||
regex: &'a RegexSearch,
|
regex: &'a RegexSearch,
|
||||||
@ -1044,7 +1234,115 @@ fn all_search_matches<'a, T>(
|
|||||||
RegexIter::new(start, end, AlacDirection::Right, term, regex)
|
RegexIter::new(start, end, AlacDirection::Right, term, regex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn content_index_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> usize {
|
||||||
|
let col = min(
|
||||||
|
(pos.x() / content.size.cell_width()) as usize,
|
||||||
|
content.size.columns() - 1,
|
||||||
|
) as usize;
|
||||||
|
let line = min(
|
||||||
|
(pos.y() / content.size.line_height()) as usize,
|
||||||
|
content.size.screen_lines() - 1,
|
||||||
|
) as usize;
|
||||||
|
|
||||||
|
line * content.size.columns() + col
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_uri(uri: &str) -> Result<(), std::io::Error> {
|
||||||
|
let mut command = Command::new("open");
|
||||||
|
command.arg(uri);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
command
|
||||||
|
.pre_exec(|| {
|
||||||
|
match libc::fork() {
|
||||||
|
-1 => return Err(io::Error::last_os_error()),
|
||||||
|
0 => (),
|
||||||
|
_ => libc::_exit(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
if libc::setsid() == -1 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.spawn()?
|
||||||
|
.wait()
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use gpui::geometry::vector::vec2f;
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
|
use crate::content_index_for_mouse;
|
||||||
|
|
||||||
|
use self::terminal_test_context::TerminalTestContext;
|
||||||
|
|
||||||
pub mod terminal_test_context;
|
pub mod terminal_test_context;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mouse_to_cell() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
let viewport_cells = rng.gen_range(5..50);
|
||||||
|
let cell_size = rng.gen_range(5.0..20.0);
|
||||||
|
|
||||||
|
let size = crate::TerminalSize {
|
||||||
|
cell_width: cell_size,
|
||||||
|
line_height: cell_size,
|
||||||
|
height: cell_size * (viewport_cells as f32),
|
||||||
|
width: cell_size * (viewport_cells as f32),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng);
|
||||||
|
|
||||||
|
for i in 0..(viewport_cells - 1) {
|
||||||
|
let i = i as usize;
|
||||||
|
for j in 0..(viewport_cells - 1) {
|
||||||
|
let j = j as usize;
|
||||||
|
let min_row = i as f32 * cell_size;
|
||||||
|
let max_row = (i + 1) as f32 * cell_size;
|
||||||
|
let min_col = j as f32 * cell_size;
|
||||||
|
let max_col = (j + 1) as f32 * cell_size;
|
||||||
|
|
||||||
|
let mouse_pos = vec2f(
|
||||||
|
rng.gen_range(min_row..max_row),
|
||||||
|
rng.gen_range(min_col..max_col),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
content.cells[content_index_for_mouse(mouse_pos, &content)].c,
|
||||||
|
cells[j][i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mouse_to_cell_clamp() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
let size = crate::TerminalSize {
|
||||||
|
cell_width: 10.,
|
||||||
|
line_height: 10.,
|
||||||
|
height: 100.,
|
||||||
|
width: 100.,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
content.cells[content_index_for_mouse(vec2f(-10., -10.), &content)].c,
|
||||||
|
cells[0][0]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
content.cells[content_index_for_mouse(vec2f(1000., 1000.), &content)].c,
|
||||||
|
cells[9][9]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ use util::truncate_and_trailoff;
|
|||||||
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
|
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
|
||||||
use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace};
|
use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace};
|
||||||
|
|
||||||
use crate::TerminalSize;
|
|
||||||
use project::{LocalWorktree, Project, ProjectPath};
|
use project::{LocalWorktree, Project, ProjectPath};
|
||||||
use settings::{AlternateScroll, Settings, WorkingDirectory};
|
use settings::{AlternateScroll, Settings, WorkingDirectory};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
@ -87,9 +86,6 @@ impl TerminalContainer {
|
|||||||
modal: bool,
|
modal: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
//The exact size here doesn't matter, the terminal will be resized on the first layout
|
|
||||||
let size_info = TerminalSize::default();
|
|
||||||
|
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
let shell = settings.terminal_overrides.shell.clone();
|
let shell = settings.terminal_overrides.shell.clone();
|
||||||
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
|
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
|
||||||
@ -111,7 +107,6 @@ impl TerminalContainer {
|
|||||||
working_directory.clone(),
|
working_directory.clone(),
|
||||||
shell,
|
shell,
|
||||||
envs,
|
envs,
|
||||||
size_info,
|
|
||||||
settings.terminal_overrides.blinking.clone(),
|
settings.terminal_overrides.blinking.clone(),
|
||||||
scroll,
|
scroll,
|
||||||
cx.window_id(),
|
cx.window_id(),
|
||||||
|
@ -7,15 +7,17 @@ use alacritty_terminal::{
|
|||||||
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
|
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
|
elements::{Empty, Overlay},
|
||||||
|
fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight},
|
||||||
geometry::{
|
geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
serde_json::json,
|
serde_json::json,
|
||||||
text_layout::{Line, RunStyle},
|
text_layout::{Line, RunStyle},
|
||||||
Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion,
|
Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext,
|
||||||
PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle,
|
ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint,
|
||||||
|
TextLayoutCache, WeakModelHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
@ -42,6 +44,7 @@ pub struct LayoutState {
|
|||||||
size: TerminalSize,
|
size: TerminalSize,
|
||||||
mode: TermMode,
|
mode: TermMode,
|
||||||
display_offset: usize,
|
display_offset: usize,
|
||||||
|
hyperlink_tooltip: Option<ElementBox>,
|
||||||
}
|
}
|
||||||
|
|
||||||
///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
|
///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
|
||||||
@ -180,6 +183,7 @@ impl TerminalElement {
|
|||||||
text_layout_cache: &TextLayoutCache,
|
text_layout_cache: &TextLayoutCache,
|
||||||
font_cache: &FontCache,
|
font_cache: &FontCache,
|
||||||
modal: bool,
|
modal: bool,
|
||||||
|
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
|
||||||
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
|
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
|
||||||
let mut cells = vec![];
|
let mut cells = vec![];
|
||||||
let mut rects = vec![];
|
let mut rects = vec![];
|
||||||
@ -237,7 +241,7 @@ impl TerminalElement {
|
|||||||
//Layout current cell text
|
//Layout current cell text
|
||||||
{
|
{
|
||||||
let cell_text = &cell.c.to_string();
|
let cell_text = &cell.c.to_string();
|
||||||
if cell_text != " " {
|
if !is_blank(&cell) {
|
||||||
let cell_style = TerminalElement::cell_style(
|
let cell_style = TerminalElement::cell_style(
|
||||||
&cell,
|
&cell,
|
||||||
fg,
|
fg,
|
||||||
@ -245,6 +249,7 @@ impl TerminalElement {
|
|||||||
text_style,
|
text_style,
|
||||||
font_cache,
|
font_cache,
|
||||||
modal,
|
modal,
|
||||||
|
hyperlink,
|
||||||
);
|
);
|
||||||
|
|
||||||
let layout_cell = text_layout_cache.layout_str(
|
let layout_cell = text_layout_cache.layout_str(
|
||||||
@ -257,8 +262,8 @@ impl TerminalElement {
|
|||||||
Point::new(line_index as i32, cell.point.column.0 as i32),
|
Point::new(line_index as i32, cell.point.column.0 as i32),
|
||||||
layout_cell,
|
layout_cell,
|
||||||
))
|
))
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cur_rect.is_some() {
|
if cur_rect.is_some() {
|
||||||
@ -304,11 +309,12 @@ impl TerminalElement {
|
|||||||
text_style: &TextStyle,
|
text_style: &TextStyle,
|
||||||
font_cache: &FontCache,
|
font_cache: &FontCache,
|
||||||
modal: bool,
|
modal: bool,
|
||||||
|
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
|
||||||
) -> RunStyle {
|
) -> RunStyle {
|
||||||
let flags = indexed.cell.flags;
|
let flags = indexed.cell.flags;
|
||||||
let fg = convert_color(&fg, &style.colors, modal);
|
let fg = convert_color(&fg, &style.colors, modal);
|
||||||
|
|
||||||
let underline = flags
|
let mut underline = flags
|
||||||
.intersects(Flags::ALL_UNDERLINES)
|
.intersects(Flags::ALL_UNDERLINES)
|
||||||
.then(|| Underline {
|
.then(|| Underline {
|
||||||
color: Some(fg),
|
color: Some(fg),
|
||||||
@ -317,6 +323,12 @@ impl TerminalElement {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if indexed.cell.hyperlink().is_some() {
|
||||||
|
if underline.thickness == OrderedFloat(0.) {
|
||||||
|
underline.thickness = OrderedFloat(1.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut properties = Properties::new();
|
let mut properties = Properties::new();
|
||||||
if indexed
|
if indexed
|
||||||
.flags
|
.flags
|
||||||
@ -332,11 +344,25 @@ impl TerminalElement {
|
|||||||
.select_font(text_style.font_family_id, &properties)
|
.select_font(text_style.font_family_id, &properties)
|
||||||
.unwrap_or(text_style.font_id);
|
.unwrap_or(text_style.font_id);
|
||||||
|
|
||||||
RunStyle {
|
let mut result = RunStyle {
|
||||||
color: fg,
|
color: fg,
|
||||||
font_id,
|
font_id,
|
||||||
underline,
|
underline,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((style, range)) = hyperlink {
|
||||||
|
if range.contains(&indexed.point) {
|
||||||
|
if let Some(underline) = style.underline {
|
||||||
|
result.underline = underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(color) = style.color {
|
||||||
|
result.color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generic_button_handler<E>(
|
fn generic_button_handler<E>(
|
||||||
@ -366,7 +392,7 @@ impl TerminalElement {
|
|||||||
) {
|
) {
|
||||||
let connection = self.terminal;
|
let connection = self.terminal;
|
||||||
|
|
||||||
let mut region = MouseRegion::new::<Self>(view_id, view_id, visible_bounds);
|
let mut region = MouseRegion::new::<Self>(view_id, 0, visible_bounds);
|
||||||
|
|
||||||
// Terminal Emulator controlled behavior:
|
// Terminal Emulator controlled behavior:
|
||||||
region = region
|
region = region
|
||||||
@ -428,6 +454,16 @@ impl TerminalElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.on_move(move |event, cx| {
|
||||||
|
if cx.is_parent_view_focused() {
|
||||||
|
if let Some(conn_handle) = connection.upgrade(cx.app) {
|
||||||
|
conn_handle.update(cx.app, |terminal, cx| {
|
||||||
|
terminal.mouse_move(&event, origin);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.on_scroll(TerminalElement::generic_button_handler(
|
.on_scroll(TerminalElement::generic_button_handler(
|
||||||
connection,
|
connection,
|
||||||
origin,
|
origin,
|
||||||
@ -481,21 +517,6 @@ impl TerminalElement {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
//Mouse move manages both dragging and motion events
|
|
||||||
if mode.intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION) {
|
|
||||||
region = region
|
|
||||||
//TODO: This does not fire on right-mouse-down-move events.
|
|
||||||
.on_move(move |event, cx| {
|
|
||||||
if cx.is_parent_view_focused() {
|
|
||||||
if let Some(conn_handle) = connection.upgrade(cx.app) {
|
|
||||||
conn_handle.update(cx.app, |terminal, cx| {
|
|
||||||
terminal.mouse_move(&event, origin);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.scene.push_mouse_region(region);
|
cx.scene.push_mouse_region(region);
|
||||||
}
|
}
|
||||||
@ -547,6 +568,9 @@ impl Element for TerminalElement {
|
|||||||
|
|
||||||
//Setup layout information
|
//Setup layout information
|
||||||
let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
|
let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
|
||||||
|
let link_style = settings.theme.editor.link_definition;
|
||||||
|
let tooltip_style = settings.theme.tooltip.clone();
|
||||||
|
|
||||||
let text_style = TerminalElement::make_text_style(font_cache, settings);
|
let text_style = TerminalElement::make_text_style(font_cache, settings);
|
||||||
let selection_color = settings.theme.editor.selection.selection;
|
let selection_color = settings.theme.editor.selection.selection;
|
||||||
let match_color = settings.theme.search.match_background;
|
let match_color = settings.theme.search.match_background;
|
||||||
@ -569,9 +593,34 @@ impl Element for TerminalElement {
|
|||||||
};
|
};
|
||||||
let terminal_handle = self.terminal.upgrade(cx).unwrap();
|
let terminal_handle = self.terminal.upgrade(cx).unwrap();
|
||||||
|
|
||||||
terminal_handle.update(cx.app, |terminal, cx| {
|
let last_hovered_hyperlink = 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()
|
||||||
|
});
|
||||||
|
|
||||||
|
let view_handle = self.view.clone();
|
||||||
|
let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _, id)| {
|
||||||
|
// last_mouse.and_then(|_last_mouse| {
|
||||||
|
view_handle.upgrade(cx).map(|handle| {
|
||||||
|
let mut tooltip = cx.render(&handle, |_, cx| {
|
||||||
|
Overlay::new(
|
||||||
|
Empty::new()
|
||||||
|
.contained()
|
||||||
|
.constrained()
|
||||||
|
.with_width(dimensions.width())
|
||||||
|
.with_height(dimensions.height())
|
||||||
|
.with_tooltip::<TerminalElement, _>(id, uri, None, tooltip_style, cx)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_position_mode(gpui::elements::OverlayPositionMode::Local)
|
||||||
|
.boxed()
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
|
||||||
|
tooltip
|
||||||
|
})
|
||||||
|
// })
|
||||||
});
|
});
|
||||||
|
|
||||||
let TerminalContent {
|
let TerminalContent {
|
||||||
@ -581,8 +630,9 @@ impl Element for TerminalElement {
|
|||||||
cursor_char,
|
cursor_char,
|
||||||
selection,
|
selection,
|
||||||
cursor,
|
cursor,
|
||||||
|
last_hovered_hyperlink,
|
||||||
..
|
..
|
||||||
} = &terminal_handle.read(cx).last_content;
|
} = { &terminal_handle.read(cx).last_content };
|
||||||
|
|
||||||
// searches, highlights to a single range representations
|
// searches, highlights to a single range representations
|
||||||
let mut relative_highlighted_ranges = Vec::new();
|
let mut relative_highlighted_ranges = Vec::new();
|
||||||
@ -602,6 +652,9 @@ impl Element for TerminalElement {
|
|||||||
cx.text_layout_cache,
|
cx.text_layout_cache,
|
||||||
cx.font_cache(),
|
cx.font_cache(),
|
||||||
self.modal,
|
self.modal,
|
||||||
|
last_hovered_hyperlink
|
||||||
|
.as_ref()
|
||||||
|
.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
|
||||||
@ -633,10 +686,11 @@ impl Element for TerminalElement {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let focused = self.focused;
|
||||||
TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
|
TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
|
||||||
move |(cursor_position, block_width)| {
|
move |(cursor_position, block_width)| {
|
||||||
let shape = match cursor.shape {
|
let shape = match cursor.shape {
|
||||||
AlacCursorShape::Block if !self.focused => CursorShape::Hollow,
|
AlacCursorShape::Block if !focused => CursorShape::Hollow,
|
||||||
AlacCursorShape::Block => CursorShape::Block,
|
AlacCursorShape::Block => CursorShape::Block,
|
||||||
AlacCursorShape::Underline => CursorShape::Underscore,
|
AlacCursorShape::Underline => CursorShape::Underscore,
|
||||||
AlacCursorShape::Beam => CursorShape::Bar,
|
AlacCursorShape::Beam => CursorShape::Bar,
|
||||||
@ -669,6 +723,7 @@ impl Element for TerminalElement {
|
|||||||
relative_highlighted_ranges,
|
relative_highlighted_ranges,
|
||||||
mode: *mode,
|
mode: *mode,
|
||||||
display_offset: *display_offset,
|
display_offset: *display_offset,
|
||||||
|
hyperlink_tooltip,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -691,7 +746,11 @@ impl Element for TerminalElement {
|
|||||||
|
|
||||||
cx.scene.push_cursor_region(gpui::CursorRegion {
|
cx.scene.push_cursor_region(gpui::CursorRegion {
|
||||||
bounds,
|
bounds,
|
||||||
style: gpui::CursorStyle::IBeam,
|
style: if layout.hyperlink_tooltip.is_some() {
|
||||||
|
gpui::CursorStyle::PointingHand
|
||||||
|
} else {
|
||||||
|
gpui::CursorStyle::IBeam
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.paint_layer(clip_bounds, |cx| {
|
cx.paint_layer(clip_bounds, |cx| {
|
||||||
@ -743,6 +802,10 @@ impl Element for TerminalElement {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(element) = &mut layout.hyperlink_tooltip {
|
||||||
|
element.paint(origin, visible_bounds, cx)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -781,6 +844,18 @@ impl Element for TerminalElement {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
|
} else if let Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) = event {
|
||||||
|
self.terminal
|
||||||
|
.upgrade(cx.app)
|
||||||
|
.map(|model_handle| {
|
||||||
|
if model_handle.update(cx.app, |term, _| term.refresh_hyperlink(*cmd)) {
|
||||||
|
cx.notify();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -824,6 +899,29 @@ impl Element for TerminalElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_blank(cell: &IndexedCell) -> bool {
|
||||||
|
if cell.c != ' ' {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cell.bg != AnsiColor::Named(NamedColor::Background) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cell.hyperlink().is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cell
|
||||||
|
.flags
|
||||||
|
.intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
fn to_highlighted_range_lines(
|
fn to_highlighted_range_lines(
|
||||||
range: &RangeInclusive<Point>,
|
range: &RangeInclusive<Point>,
|
||||||
layout: &LayoutState,
|
layout: &LayoutState,
|
||||||
|
@ -362,7 +362,9 @@ impl View for TerminalView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
self.terminal.read(cx).focus_out();
|
self.terminal.update(cx, |terminal, _| {
|
||||||
|
terminal.focus_out();
|
||||||
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
use std::{path::Path, time::Duration};
|
use std::{path::Path, time::Duration};
|
||||||
|
|
||||||
|
use alacritty_terminal::{
|
||||||
|
index::{Column, Line, Point},
|
||||||
|
term::cell::Cell,
|
||||||
|
};
|
||||||
use gpui::{ModelHandle, TestAppContext, ViewHandle};
|
use gpui::{ModelHandle, TestAppContext, ViewHandle};
|
||||||
|
|
||||||
use project::{Entry, Project, ProjectPath, Worktree};
|
use project::{Entry, Project, ProjectPath, Worktree};
|
||||||
|
use rand::{rngs::ThreadRng, Rng};
|
||||||
use workspace::{AppState, Workspace};
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
|
use crate::{IndexedCell, TerminalContent, TerminalSize};
|
||||||
|
|
||||||
pub struct TerminalTestContext<'a> {
|
pub struct TerminalTestContext<'a> {
|
||||||
pub cx: &'a mut TestAppContext,
|
pub cx: &'a mut TestAppContext,
|
||||||
}
|
}
|
||||||
@ -88,6 +95,39 @@ impl<'a> TerminalTestContext<'a> {
|
|||||||
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
|
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_terminal_content(
|
||||||
|
size: TerminalSize,
|
||||||
|
rng: &mut ThreadRng,
|
||||||
|
) -> (TerminalContent, Vec<Vec<char>>) {
|
||||||
|
let mut ic = Vec::new();
|
||||||
|
let mut cells = Vec::new();
|
||||||
|
|
||||||
|
for row in 0..((size.height() / size.line_height()) as usize) {
|
||||||
|
let mut row_vec = Vec::new();
|
||||||
|
for col in 0..((size.width() / size.cell_width()) as usize) {
|
||||||
|
let cell_char = rng.gen();
|
||||||
|
ic.push(IndexedCell {
|
||||||
|
point: Point::new(Line(row as i32), Column(col)),
|
||||||
|
cell: Cell {
|
||||||
|
c: cell_char,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
row_vec.push(cell_char)
|
||||||
|
}
|
||||||
|
cells.push(row_vec)
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
TerminalContent {
|
||||||
|
cells: ic,
|
||||||
|
size,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
cells,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Drop for TerminalTestContext<'a> {
|
impl<'a> Drop for TerminalTestContext<'a> {
|
||||||
|
Loading…
Reference in New Issue
Block a user