Merge branch 'main' into elevations

This commit is contained in:
K Simmons 2022-10-18 12:47:15 -07:00
commit b0ddbeb0ad
273 changed files with 26504 additions and 15624 deletions

View file

@ -29,9 +29,14 @@ shellexpand = "2.1.0"
libc = "0.2"
anyhow = "1"
thiserror = "1.0"
lazy_static = "1.4.0"
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
client = { path = "../client", features = ["test-support"]}
project = { path = "../project", features = ["test-support"]}
workspace = { path = "../workspace", features = ["test-support"] }
rand = "0.8.5"

View file

@ -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 = min(GridCol(col as usize), cur_size.last_column());
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)]
mod test {
use crate::mappings::mouse::mouse_point;
use crate::mappings::mouse::grid_point;
#[test]
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 origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
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!(
point,
alacritty_terminal::index::Point::new(

View file

@ -29,18 +29,22 @@ use futures::{
};
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 settings::{AlternateScroll, Settings, Shell, TerminalBlink};
use util::ResultExt;
use std::{
cmp::min,
collections::{HashMap, VecDeque},
fmt::Display,
ops::{Deref, RangeInclusive, Sub},
os::unix::prelude::AsRawFd,
io,
ops::{Deref, Index, RangeInclusive, Sub},
os::unix::{prelude::AsRawFd, process::CommandExt},
path::PathBuf,
process::Command,
sync::Arc,
time::{Duration, Instant},
};
@ -49,9 +53,7 @@ use thiserror::Error;
use gpui::{
geometry::vector::{vec2f, Vector2F},
keymap::Keystroke,
scene::{
ClickRegionEvent, DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent,
},
scene::{DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent},
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task,
};
@ -59,6 +61,7 @@ use crate::mappings::{
colors::{get_color_at_index, to_alac_rgb},
keys::to_esc_str,
};
use lazy_static::lazy_static;
///Initialize and register all of our action handlers
pub fn init(cx: &mut MutableAppContext) {
@ -70,12 +73,18 @@ pub fn init(cx: &mut MutableAppContext) {
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
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_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! {
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
#[derive(Clone, Copy, Debug)]
pub enum Event {
@ -98,6 +107,8 @@ enum InternalEvent {
ScrollToPoint(Point),
SetSelection(Option<(Selection, Point)>),
UpdateSelection(Vector2F),
// Adjusted mouse position, should open
FindHyperlink(Vector2F, bool),
Copy,
}
@ -267,7 +278,6 @@ impl TerminalBuilder {
working_directory: Option<PathBuf>,
shell: Option<Shell>,
env: Option<HashMap<String, String>>,
initial_size: TerminalSize,
blink_settings: Option<TerminalBlink>,
alternate_scroll: &AlternateScroll,
window_id: usize,
@ -307,7 +317,11 @@ impl TerminalBuilder {
//TODO: Remove with a bounded sender which can be dispatched on &self
let (events_tx, events_rx) = unbounded();
//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
if let Some(TerminalBlink::On) = blink_settings {
@ -322,7 +336,11 @@ impl TerminalBuilder {
let term = Arc::new(FairMutex::new(term));
//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,
Err(error) => {
bail!(TerminalError {
@ -354,7 +372,6 @@ impl TerminalBuilder {
term,
events: VecDeque::with_capacity(10), //Should never get this high.
last_content: Default::default(),
cur_size: initial_size,
last_mouse: None,
matches: Vec::new(),
last_synced: Instant::now(),
@ -365,6 +382,9 @@ impl TerminalBuilder {
foreground_process_info: None,
breadcrumb_text: String::new(),
scroll_px: 0.,
last_mouse_position: None,
next_link_id: 0,
selection_phase: SelectionPhase::Ended,
};
Ok(TerminalBuilder {
@ -450,6 +470,8 @@ pub struct TerminalContent {
selection: Option<SelectionRange>,
cursor: RenderableCursor,
cursor_char: char,
size: TerminalSize,
last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
}
impl Default for TerminalContent {
@ -465,17 +487,27 @@ impl Default for TerminalContent {
point: Point::new(Line(0), Column(0)),
},
cursor_char: Default::default(),
size: Default::default(),
last_hovered_hyperlink: None,
}
}
}
#[derive(PartialEq, Eq)]
pub enum SelectionPhase {
Selecting,
Ended,
}
pub struct Terminal {
pty_tx: Notifier,
term: Arc<FairMutex<Term<ZedListener>>>,
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
last_mouse_position: Option<Vector2F>,
pub matches: Vec<RangeInclusive<Point>>,
cur_size: TerminalSize,
last_content: TerminalContent,
last_synced: Instant,
sync_task: Option<Task<()>>,
@ -485,6 +517,8 @@ pub struct Terminal {
shell_fd: u32,
foreground_process_info: Option<LocalProcessInfo>,
scroll_px: f32,
next_link_id: usize,
selection_phase: SelectionPhase,
}
impl Terminal {
@ -508,7 +542,7 @@ impl Terminal {
)),
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
AlacTermEvent::TextAreaSizeRequest(format) => {
self.write_to_pty(format(self.cur_size.into()))
self.write_to_pty(format(self.last_content.size.into()))
}
AlacTermEvent::CursorBlinkingChange => {
cx.emit(Event::BlinkChanged);
@ -577,24 +611,45 @@ impl Terminal {
new_size.height = f32::max(new_size.line_height, new_size.height);
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();
// When this resize happens
// We go from 737px -> 703px height
// This means there is 1 less line
// that means the delta is 1
// That means the selection is rotated by -1
term.resize(new_size);
}
InternalEvent::Clear => {
self.write_to_pty("\x0c".to_string());
// Clear back buffer
term.clear_screen(ClearMode::Saved);
let cursor = term.grid().cursor.point;
// Clear the lines above
term.grid_mut().reset_region(..cursor.line);
// Copy the current line up
let line = term.grid()[cursor.line][..cursor.column]
.iter()
.cloned()
.enumerate()
.collect::<Vec<(usize, Cell)>>();
for (i, cell) in line {
term.grid_mut()[Line(0)][Column(i)] = cell;
}
// Reset the cursor
term.grid_mut().cursor.point =
Point::new(Line(0), term.grid_mut().cursor.point.column);
let new_cursor = term.grid().cursor.point;
// Clear the lines below the new cursor
if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
term.grid_mut().reset_region((new_cursor.line + 1)..);
}
}
InternalEvent::Scroll(scroll) => {
term.scroll_display(*scroll);
self.refresh_hyperlink();
}
InternalEvent::SetSelection(selection) => {
term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
@ -606,8 +661,12 @@ impl Terminal {
}
InternalEvent::UpdateSelection(position) => {
if let Some(mut selection) = term.selection.take() {
let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
let side = mouse_side(*position, self.cur_size);
let point = grid_point(
*position,
self.last_content.size,
term.grid().display_offset(),
);
let side = mouse_side(*position, self.last_content.size);
selection.update(point, side);
term.selection = Some(selection);
@ -622,10 +681,95 @@ impl Terminal {
cx.write_to_clipboard(ClipboardItem::new(txt))
}
}
InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
InternalEvent::ScrollToPoint(point) => {
term.scroll_to_point(*point);
self.refresh_hyperlink();
}
InternalEvent::FindHyperlink(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(),
)
.grid_clamp(term, alacritty_terminal::index::Boundary::Cursor);
let link = term.grid().index(point).hyperlink();
let found_url = if link.is_some() {
let mut min_index = point;
loop {
let new_min_index =
min_index.sub(term, alacritty_terminal::index::Boundary::Cursor, 1);
if new_min_index == min_index {
break;
} else if term.grid().index(new_min_index).hyperlink() != link {
break;
} else {
min_index = new_min_index
}
}
let mut max_index = point;
loop {
let new_max_index =
max_index.add(term, alacritty_terminal::index::Boundary::Cursor, 1);
if new_max_index == max_index {
break;
} else if term.grid().index(new_max_index).hyperlink() != link {
break;
} else {
max_index = new_max_index
}
}
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, url_match))
} else {
None
};
if let Some((url, url_match)) = found_url {
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 {
&self.last_content
}
@ -691,7 +835,8 @@ impl Terminal {
} else {
text.replace("\r\n", "\r").replace('\n', "\r")
};
self.input(paste_text)
self.input(paste_text);
}
pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
@ -730,11 +875,11 @@ impl Terminal {
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();
}
fn make_content(term: &Term<ZedListener>) -> TerminalContent {
fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
let content = term.renderable_content();
TerminalContent {
cells: content
@ -757,6 +902,8 @@ impl Terminal {
selection: content.selection,
cursor: content.cursor,
cursor_char: term.grid()[content.cursor.point].c,
size: last_content.size,
last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
}
}
@ -766,7 +913,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) {
self.write_to_pty("\x1b[O".to_string());
}
@ -795,21 +943,40 @@ impl Terminal {
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
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);
let side = mouse_side(position, self.cur_size);
if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
self.pty_tx.notify(bytes);
if self.mouse_changed(point, side) {
if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
self.pty_tx.notify(bytes);
}
}
} else {
self.hyperlink_from_position(Some(position));
}
}
fn hyperlink_from_position(&mut self, position: Option<Vector2F>) {
if self.selection_phase == SelectionPhase::Selecting {
self.last_content.last_hovered_hyperlink = None;
} else if let Some(position) = position {
self.events
.push_back(InternalEvent::FindHyperlink(position, false));
}
}
pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
self.last_mouse_position = Some(position);
if !self.mouse_mode(e.shift) {
self.selection_phase = SelectionPhase::Selecting;
// Alacritty has the same ordering, of first updating the selection
// then scrolling 15ms later
self.events
@ -822,20 +989,18 @@ impl Terminal {
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
.push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
self.events
.push_back(InternalEvent::UpdateSelection(position))
}
}
}
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
let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
let bottom = e.region.lower_left().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.last_content.size.line_height * 2.);
let scroll_delta = if e.position.y() < top {
(top - e.position.y()).powf(1.1)
} else if e.position.y() > bottom {
@ -848,27 +1013,24 @@ impl Terminal {
pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
let side = mouse_side(position, self.cur_size);
let point = grid_point(
position,
self.last_content.size,
self.last_content.display_offset,
);
if self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
self.pty_tx.notify(bytes);
}
} else if e.button == MouseButton::Left {
self.events.push_back(InternalEvent::SetSelection(Some((
Selection::new(SelectionType::Simple, point, side),
point,
))));
}
}
pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
if !self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
let side = mouse_side(position, self.cur_size);
let position = e.position.sub(origin);
let point = grid_point(
position,
self.last_content.size,
self.last_content.display_offset,
);
let side = mouse_side(position, self.last_content.size);
let selection_type = match e.click_count {
0 => return, //This is a release
@ -888,19 +1050,47 @@ impl Terminal {
}
}
pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F, cx: &mut ModelContext<Self>) {
let settings = cx.global::<Settings>();
let copy_on_select = settings
.terminal_overrides
.copy_on_select
.unwrap_or_else(|| {
settings
.terminal_defaults
.copy_on_select
.expect("Should be set in defaults")
});
let position = e.position.sub(origin);
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) {
self.pty_tx.notify(bytes);
}
} else if e.button == MouseButton::Left {
// Seems pretty standard to automatically copy on mouse_up for terminals,
// so let's do that here
self.copy();
} else {
if e.button == MouseButton::Left && copy_on_select {
self.copy();
}
//Hyperlinks
if self.selection_phase == SelectionPhase::Ended {
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::FindHyperlink(position, true));
}
}
}
self.selection_phase = SelectionPhase::Ended;
self.last_mouse = None;
}
@ -910,9 +1100,9 @@ impl Terminal {
if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
if mouse_mode {
let point = mouse_point(
let point = grid_point(
e.position.sub(origin),
self.cur_size,
self.last_content.size,
self.last_content.display_offset,
);
@ -940,6 +1130,10 @@ impl Terminal {
}
}
pub fn refresh_hyperlink(&mut self) {
self.hyperlink_from_position(self.last_mouse_position);
}
fn determine_scroll_lines(
&mut self,
e: &ScrollWheelRegionEvent,
@ -955,20 +1149,22 @@ impl Terminal {
}
/* Calculate the appropriate scroll lines */
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;
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
// 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)
}
/* 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,
}
}
@ -1011,30 +1207,36 @@ impl Entity for Terminal {
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 {
let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
selection.update(*range.end(), AlacDirection::Right);
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>(
term: &'a Term<T>,
regex: &'a RegexSearch,
@ -1044,7 +1246,115 @@ fn all_search_matches<'a, T>(
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)]
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;
#[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]
);
}
}

View file

@ -11,7 +11,6 @@ use util::truncate_and_trailoff;
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace};
use crate::TerminalSize;
use project::{LocalWorktree, Project, ProjectPath};
use settings::{AlternateScroll, Settings, WorkingDirectory};
use smallvec::SmallVec;
@ -86,9 +85,6 @@ impl TerminalContainer {
modal: bool,
cx: &mut ViewContext<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 shell = settings.terminal_overrides.shell.clone();
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
@ -110,7 +106,6 @@ impl TerminalContainer {
working_directory.clone(),
shell,
envs,
size_info,
settings.terminal_overrides.blinking.clone(),
scroll,
cx.window_id(),
@ -162,10 +157,10 @@ impl View for TerminalContainer {
"Terminal"
}
fn render(&mut self, _cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
match &self.content {
TerminalContainerContent::Connected(connected) => ChildView::new(connected),
TerminalContainerContent::Error(error) => ChildView::new(error),
TerminalContainerContent::Connected(connected) => ChildView::new(connected, cx),
TerminalContainerContent::Error(error) => ChildView::new(error, cx),
}
.boxed()
}

View file

@ -7,15 +7,17 @@ use alacritty_terminal::{
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
use gpui::{
color::Color,
fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
elements::{Empty, Overlay},
fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
serde_json::json,
text_layout::{Line, RunStyle},
Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion,
PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle,
Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton,
MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache, WeakModelHandle,
WeakViewHandle,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
@ -42,6 +44,7 @@ pub struct LayoutState {
size: TerminalSize,
mode: TermMode,
display_offset: usize,
hyperlink_tooltip: Option<ElementBox>,
}
///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
@ -176,6 +179,7 @@ impl TerminalElement {
terminal_theme: &TerminalStyle,
text_layout_cache: &TextLayoutCache,
font_cache: &FontCache,
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
let mut cells = vec![];
let mut rects = vec![];
@ -233,13 +237,14 @@ impl TerminalElement {
//Layout current cell text
{
let cell_text = &cell.c.to_string();
if cell_text != " " {
if !is_blank(&cell) {
let cell_style = TerminalElement::cell_style(
&cell,
fg,
terminal_theme,
text_style,
font_cache,
hyperlink,
);
let layout_cell = text_layout_cache.layout_str(
@ -252,8 +257,8 @@ impl TerminalElement {
Point::new(line_index as i32, cell.point.column.0 as i32),
layout_cell,
))
}
};
};
}
}
if cur_rect.is_some() {
@ -298,11 +303,12 @@ impl TerminalElement {
style: &TerminalStyle,
text_style: &TextStyle,
font_cache: &FontCache,
hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
) -> RunStyle {
let flags = indexed.cell.flags;
let fg = convert_color(&fg, &style);
let underline = flags
let mut underline = flags
.intersects(Flags::ALL_UNDERLINES)
.then(|| Underline {
color: Some(fg),
@ -311,14 +317,17 @@ impl TerminalElement {
})
.unwrap_or_default();
if indexed.cell.hyperlink().is_some() {
if underline.thickness == OrderedFloat(0.) {
underline.thickness = OrderedFloat(1.);
}
}
let mut properties = Properties::new();
if indexed
.flags
.intersects(Flags::BOLD | Flags::BOLD_ITALIC | Flags::DIM_BOLD)
{
if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
properties = *properties.weight(Weight::BOLD);
}
if indexed.flags.intersects(Flags::ITALIC | Flags::BOLD_ITALIC) {
if indexed.flags.intersects(Flags::ITALIC) {
properties = *properties.style(Italic);
}
@ -326,11 +335,25 @@ impl TerminalElement {
.select_font(text_style.font_family_id, &properties)
.unwrap_or(text_style.font_id);
RunStyle {
let mut result = RunStyle {
color: fg,
font_id,
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>(
@ -360,7 +383,7 @@ impl TerminalElement {
) {
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:
region = region
@ -392,19 +415,8 @@ impl TerminalElement {
TerminalElement::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_up(&e, origin);
},
),
)
// Handle click based selections
.on_click(
MouseButton::Left,
TerminalElement::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.left_click(&e, origin);
move |terminal, origin, e, cx| {
terminal.mouse_up(&e, origin, cx);
},
),
)
@ -422,13 +434,25 @@ impl TerminalElement {
});
}
})
.on_scroll(TerminalElement::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.scroll_wheel(e, origin);
},
));
.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(move |event, cx| {
// cx.focus_parent_view();
if let Some(conn_handle) = connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
terminal.scroll_wheel(event, origin);
cx.notify();
})
}
});
// Mouse mode handlers:
// All mouse modes need the extra click handlers
@ -459,8 +483,8 @@ impl TerminalElement {
TerminalElement::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_up(&e, origin);
move |terminal, origin, e, cx| {
terminal.mouse_up(&e, origin, cx);
},
),
)
@ -469,27 +493,12 @@ impl TerminalElement {
TerminalElement::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_up(&e, origin);
move |terminal, origin, e, cx| {
terminal.mouse_up(&e, origin, cx);
},
),
)
}
//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);
}
@ -541,6 +550,9 @@ impl Element for TerminalElement {
//Setup layout information
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 selection_color = settings.theme.editor.selection.selection;
let match_color = settings.theme.search.match_background;
@ -559,9 +571,34 @@ impl Element for TerminalElement {
let background_color = terminal_theme.background;
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.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 {
@ -571,8 +608,9 @@ impl Element for TerminalElement {
cursor_char,
selection,
cursor,
last_hovered_hyperlink,
..
} = &terminal_handle.read(cx).last_content;
} = { &terminal_handle.read(cx).last_content };
// searches, highlights to a single range representations
let mut relative_highlighted_ranges = Vec::new();
@ -591,6 +629,9 @@ impl Element for TerminalElement {
&terminal_theme,
cx.text_layout_cache,
cx.font_cache(),
last_hovered_hyperlink
.as_ref()
.map(|(_, range, _)| (link_style, range)),
);
//Layout cursor. Rectangle is used for IME, so we should lay it out even
@ -622,14 +663,15 @@ impl Element for TerminalElement {
)
};
let focused = self.focused;
TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
move |(cursor_position, block_width)| {
let shape = match cursor.shape {
AlacCursorShape::Block if !self.focused => CursorShape::Hollow,
AlacCursorShape::Block => CursorShape::Block,
AlacCursorShape::Underline => CursorShape::Underscore,
AlacCursorShape::Beam => CursorShape::Bar,
AlacCursorShape::HollowBlock => CursorShape::Hollow,
let (shape, text) = match cursor.shape {
AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
AlacCursorShape::Underline => (CursorShape::Underscore, None),
AlacCursorShape::Beam => (CursorShape::Bar, None),
AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
//This case is handled in the if wrapping the whole cursor layout
AlacCursorShape::Hidden => unreachable!(),
};
@ -640,7 +682,7 @@ impl Element for TerminalElement {
dimensions.line_height,
terminal_theme.cursor,
shape,
Some(cursor_text),
text,
)
},
)
@ -658,6 +700,7 @@ impl Element for TerminalElement {
relative_highlighted_ranges,
mode: *mode,
display_offset: *display_offset,
hyperlink_tooltip,
},
)
}
@ -669,6 +712,8 @@ impl Element for TerminalElement {
layout: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
//Setup element stuff
let clip_bounds = Some(visible_bounds);
@ -680,7 +725,11 @@ impl Element for TerminalElement {
cx.scene.push_cursor_region(gpui::CursorRegion {
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| {
@ -732,6 +781,10 @@ impl Element for TerminalElement {
})
}
}
if let Some(element) = &mut layout.hyperlink_tooltip {
element.paint(origin, visible_bounds, cx)
}
});
}
@ -813,6 +866,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(
range: &RangeInclusive<Point>,
layout: &LayoutState,

View file

@ -6,13 +6,15 @@ use gpui::{
actions,
elements::{AnchorCorner, ChildView, ParentElement, Stack},
geometry::vector::Vector2F,
impl_internal_actions,
impl_actions, impl_internal_actions,
keymap::Keystroke,
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
View, ViewContext, ViewHandle,
};
use serde::Deserialize;
use settings::{Settings, TerminalBlink};
use smol::Timer;
use util::ResultExt;
use workspace::pane;
use crate::{terminal_element::TerminalElement, Event, Terminal};
@ -28,6 +30,12 @@ pub struct DeployContextMenu {
pub position: Vector2F,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct SendText(String);
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct SendKeystroke(String);
actions!(
terminal,
[
@ -43,16 +51,15 @@ actions!(
SearchTest
]
);
impl_actions!(terminal, [SendText, SendKeystroke]);
impl_internal_actions!(project_panel, [DeployContextMenu]);
pub fn init(cx: &mut MutableAppContext) {
//Global binding overrrides
cx.add_action(TerminalView::ctrl_c);
cx.add_action(TerminalView::up);
cx.add_action(TerminalView::down);
cx.add_action(TerminalView::escape);
cx.add_action(TerminalView::enter);
//Useful terminal views
cx.add_action(TerminalView::send_text);
cx.add_action(TerminalView::send_keystroke);
cx.add_action(TerminalView::deploy_context_menu);
cx.add_action(TerminalView::copy);
cx.add_action(TerminalView::paste);
@ -135,8 +142,8 @@ impl TerminalView {
pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
let menu_entries = vec![
ContextMenuItem::item("Clear Buffer", Clear),
ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
ContextMenuItem::item("Clear", Clear),
ContextMenuItem::item("Close", pane::CloseActiveItem),
];
self.context_menu.update(cx, |menu, cx| {
@ -283,44 +290,26 @@ impl TerminalView {
}
}
///Synthesize the keyboard event corresponding to 'up'
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("up").unwrap(), false)
term.input(text.0.to_string());
});
}
///Synthesize the keyboard event corresponding to 'down'
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("down").unwrap(), false)
});
}
///Synthesize the keyboard event corresponding to 'ctrl-c'
fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap(), false)
});
}
///Synthesize the keyboard event corresponding to 'escape'
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("escape").unwrap(), false)
});
}
///Synthesize the keyboard event corresponding to 'enter'
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("enter").unwrap(), false)
});
fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
self.clear_bel(cx);
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&keystroke,
cx.global::<Settings>()
.terminal_overrides
.option_as_meta
.unwrap_or(false),
);
});
}
}
}
@ -349,7 +338,7 @@ impl View for TerminalView {
.contained()
.boxed(),
)
.with_child(ChildView::new(&self.context_menu).boxed())
.with_child(ChildView::new(&self.context_menu, cx).boxed())
.boxed()
}
@ -361,7 +350,9 @@ impl View for TerminalView {
}
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();
}

View file

@ -1,10 +1,17 @@
use std::{path::Path, time::Duration};
use alacritty_terminal::{
index::{Column, Line, Point},
term::cell::Cell,
};
use gpui::{ModelHandle, TestAppContext, ViewHandle};
use project::{Entry, Project, ProjectPath, Worktree};
use rand::{rngs::ThreadRng, Rng};
use workspace::{AppState, Workspace};
use crate::{IndexedCell, TerminalContent, TerminalSize};
pub struct TerminalTestContext<'a> {
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));
});
}
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> {