From c19956373ac06258f6c5041946aa854401eff949 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 16 Jul 2022 11:21:05 -0700 Subject: [PATCH 01/29] Finished bel, moving on to title --- crates/terminal/src/connection.rs | 23 +++++-- crates/terminal/src/terminal.rs | 85 +++++++------------------ crates/terminal/src/terminal_element.rs | 20 ++++-- styles/package-lock.json | 1 - 4 files changed, 55 insertions(+), 74 deletions(-) diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index 0e051da17c..748647fbef 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -3,7 +3,7 @@ mod keymappings; use alacritty_terminal::{ ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig}, - event::{Event as AlacTermEvent, Notify}, + event::{Event as AlacTermEvent, EventListener, Notify}, event_loop::{EventLoop, Msg, Notifier}, grid::Scroll, sync::FairMutex, @@ -11,16 +11,16 @@ use alacritty_terminal::{ tty::{self, setup_env}, Term, }; -use futures::{channel::mpsc::unbounded, StreamExt}; +use futures::{ + channel::mpsc::{unbounded, UnboundedSender}, + StreamExt, +}; use settings::{Settings, Shell}; use std::{collections::HashMap, path::PathBuf, sync::Arc}; use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; -use crate::{ - color_translation::{get_color_at_index, to_alac_rgb}, - ZedListener, -}; +use crate::color_translation::{get_color_at_index, to_alac_rgb}; use self::keymappings::to_esc_str; @@ -34,6 +34,17 @@ pub enum Event { Activate, Wakeup, Bell, + KeyInput, +} + +///A translation struct for Alacritty to communicate with us from their event loop +#[derive(Clone)] +pub struct ZedListener(UnboundedSender); + +impl EventListener for ZedListener { + fn send_event(&self, event: AlacTermEvent) { + self.0.unbounded_send(event).ok(); + } } pub struct TerminalConnection { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9a28f82c2d..1f25289270 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -3,15 +3,10 @@ pub mod connection; mod modal; pub mod terminal_element; -use alacritty_terminal::{ - event::{Event as AlacTermEvent, EventListener}, - term::SizeInfo, -}; +use alacritty_terminal::term::SizeInfo; use connection::{Event, TerminalConnection}; use dirs::home_dir; -use editor::Input; -use futures::channel::mpsc::UnboundedSender; use gpui::{ actions, elements::*, keymap::Keystroke, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, @@ -31,9 +26,6 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 200.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; -//For bel, use a yellow dot. (equivalent to dirty file with conflict) -//For title, introduce max title length and - ///Event to transmit the scroll from the element to the view #[derive(Clone, Debug, PartialEq)] pub struct ScrollTerminal(pub i32); @@ -67,20 +59,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(deploy_modal); cx.add_action(Terminal::copy); cx.add_action(Terminal::paste); - cx.add_action(Terminal::input); cx.add_action(Terminal::clear); } -///A translation struct for Alacritty to communicate with us from their event loop -#[derive(Clone)] -pub struct ZedListener(UnboundedSender); - -impl EventListener for ZedListener { - fn send_event(&self, event: AlacTermEvent) { - self.0.unbounded_send(event).ok(); - } -} - ///A terminal view, maintains the PTY's file handles and communicates with the terminal pub struct Terminal { connection: ModelHandle, @@ -142,6 +123,10 @@ impl Terminal { this.has_bell = true; cx.emit(Event::TitleChanged); } + // Event::Input => { + // this.has_bell = false; + // cx.emit(Event::TitleChanged); + // } _ => cx.emit(*event), }) .detach(); @@ -154,16 +139,9 @@ impl Terminal { } } - fn input(&mut self, Input(text): &Input, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - //TODO: This is probably not encoding UTF8 correctly (see alacritty/src/input.rs:L825-837) - connection.write_to_pty(text.clone()); - }); - - if self.has_bell { - self.has_bell = false; - cx.emit(Event::TitleChanged); - } + fn clear_bel(&mut self, cx: &mut ViewContext) { + self.has_bell = false; + cx.emit(Event::TitleChanged); } fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { @@ -240,8 +218,7 @@ impl View for Terminal { fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { let element = { let connection_handle = self.connection.clone().downgrade(); - let view_id = cx.view_id(); - TerminalEl::new(view_id, connection_handle, self.modal).contained() + TerminalEl::new(cx.handle(), connection_handle, self.modal).contained() }; if self.modal { @@ -274,37 +251,17 @@ impl Item for Terminal { tab_theme: &theme::Tab, cx: &gpui::AppContext, ) -> ElementBox { - let settings = cx.global::(); - let search_theme = &settings.theme.search; //TODO properly integrate themes - - let mut flex = Flex::row(); - - if self.has_bell { - flex.add_child( - Svg::new("icons/bolt_12.svg") //TODO: Swap out for a better icon, or at least resize this - .with_color(tab_theme.label.text.color) - .constrained() - .with_width(search_theme.tab_icon_width) - .aligned() - .boxed(), - ); - }; - - flex.with_child( - Label::new( - self.connection.read(cx).title.clone(), - tab_theme.label.clone(), + Flex::row() + .with_child( + Label::new( + self.connection.read(cx).title.clone(), + tab_theme.label.clone(), + ) + .aligned() + .contained() + .boxed(), ) - .aligned() - .contained() - .with_margin_left(if self.has_bell { - search_theme.tab_icon_spacing - } else { - 0. - }) - .boxed(), - ) - .boxed() + .boxed() } fn clone_on_split(&self, cx: &mut ViewContext) -> Option { @@ -365,6 +322,10 @@ impl Item for Terminal { self.has_new_content } + fn has_conflict(&self, _: &AppContext) -> bool { + self.has_bell + } + fn should_update_tab_on_event(event: &Self::Event) -> bool { matches!(event, &Event::TitleChanged) } diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index f1ba70cab9..f69827b34e 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -22,6 +22,7 @@ use gpui::{ text_layout::{Line, RunStyle}, Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, PaintContext, Quad, ScrollWheelEvent, SizeConstraint, TextLayoutCache, WeakModelHandle, + WeakViewHandle, }; use itertools::Itertools; use ordered_float::OrderedFloat; @@ -32,7 +33,11 @@ use util::ResultExt; use std::{cmp::min, ops::Range, sync::Arc}; use std::{fmt::Debug, ops::Sub}; -use crate::{color_translation::convert_color, connection::TerminalConnection, ZedListener}; +use crate::{ + color_translation::convert_color, + connection::{TerminalConnection, ZedListener}, + Terminal, +}; ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I @@ -48,7 +53,7 @@ const DEBUG_GRID: bool = false; ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalEl { connection: WeakModelHandle, - view_id: usize, + view: WeakViewHandle, modal: bool, } @@ -100,12 +105,12 @@ pub struct LayoutState { impl TerminalEl { pub fn new( - view_id: usize, + view: WeakViewHandle, connection: WeakModelHandle, modal: bool, ) -> TerminalEl { TerminalEl { - view_id, + view, connection, modal, } @@ -238,7 +243,7 @@ impl Element for TerminalEl { attach_mouse_handlers( origin, cur_size, - self.view_id, + self.view.id(), &layout.terminal, visible_bounds, cx, @@ -383,6 +388,11 @@ impl Element for TerminalEl { return false; } + //TODO Talk to keith about how to catch events emitted from an element. + if let Some(view) = self.view.upgrade(cx.app) { + view.update(cx.app, |view, cx| view.clear_bel(cx)) + } + self.connection .upgrade(cx.app) .map(|connection| { diff --git a/styles/package-lock.json b/styles/package-lock.json index 582f1c8496..5499f1852c 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": { From a4ca11ba17eef0c4453a3a3d51aae940285a303f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 16 Jul 2022 11:45:08 -0700 Subject: [PATCH 02/29] Finished design touchups --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 2 + crates/terminal/src/connection.rs | 87 ++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 97bfa15994..184eb355a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5361,6 +5361,7 @@ dependencies = [ "futures", "gpui", "itertools", + "libc", "mio-extras", "ordered-float", "project", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 09a3fb171f..6c5db4c91b 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -23,6 +23,8 @@ ordered-float = "2.1.1" itertools = "0.10" dirs = "4.0.0" shellexpand = "2.1.0" +libc = "0.2" + [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index 748647fbef..84f04e1066 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -117,6 +117,13 @@ impl TerminalConnection { } }; + let shell = { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap(); + pw.shell.to_string() + // alacritty_unix::default_shell(&pw) + }; + //And connect them together let event_loop = EventLoop::new( term.clone(), @@ -149,7 +156,7 @@ impl TerminalConnection { TerminalConnection { pty_tx: Notifier(pty_tx), term, - title: DEFAULT_TITLE.to_string(), + title: shell.to_string(), associated_directory: working_directory, } } @@ -261,3 +268,81 @@ impl Drop for TerminalConnection { impl Entity for TerminalConnection { type Event = Event; } + +mod alacritty_unix { + use alacritty_terminal::config::Program; + use gpui::anyhow::{bail, Result}; + use libc::{self}; + use std::ffi::CStr; + use std::mem::MaybeUninit; + use std::ptr; + + #[derive(Debug)] + pub struct Passwd<'a> { + _name: &'a str, + _dir: &'a str, + pub shell: &'a str, + } + + /// Return a Passwd struct with pointers into the provided buf. + /// + /// # Unsafety + /// + /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. + pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result> { + // Create zeroed passwd struct. + let mut entry: MaybeUninit = MaybeUninit::uninit(); + + let mut res: *mut libc::passwd = ptr::null_mut(); + + // Try and read the pw file. + let uid = unsafe { libc::getuid() }; + let status = unsafe { + libc::getpwuid_r( + uid, + entry.as_mut_ptr(), + buf.as_mut_ptr() as *mut _, + buf.len(), + &mut res, + ) + }; + let entry = unsafe { entry.assume_init() }; + + if status < 0 { + bail!("getpwuid_r failed"); + } + + if res.is_null() { + bail!("pw not found"); + } + + // Sanity check. + assert_eq!(entry.pw_uid, uid); + + // Build a borrowed Passwd struct. + Ok(Passwd { + _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, + _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, + shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, + }) + } + + #[cfg(target_os = "macos")] + pub fn _default_shell(pw: &Passwd<'_>) -> Program { + let shell_name = pw.shell.rsplit('/').next().unwrap(); + let argv = vec![ + String::from("-c"), + format!("exec -a -{} {}", shell_name, pw.shell), + ]; + + Program::WithArgs { + program: "/bin/bash".to_owned(), + args: argv, + } + } + + #[cfg(not(target_os = "macos"))] + pub fn default_shell(pw: &Passwd<'_>) -> Program { + Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) + } +} From c9584a9d0cde129b02fc683ce0208ef807c08ced Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 18 Jul 2022 14:47:24 -0700 Subject: [PATCH 03/29] Continuing rebases on other work --- crates/terminal/src/connection.rs | 10 +- crates/terminal/src/terminal_element.rs | 523 ++++++++++-------- .../terminal_layout_context.rs | 59 ++ 3 files changed, 348 insertions(+), 244 deletions(-) create mode 100644 crates/terminal/src/terminal_element/terminal_layout_context.rs diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index 84f04e1066..c9aa574c01 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -52,6 +52,7 @@ pub struct TerminalConnection { pub term: Arc>>, pub title: String, pub associated_directory: Option, + pub cur_size: SizeInfo, } impl TerminalConnection { @@ -157,6 +158,7 @@ impl TerminalConnection { pty_tx: Notifier(pty_tx), term, title: shell.to_string(), + cur_size: initial_size, associated_directory: working_directory, } } @@ -224,7 +226,7 @@ impl TerminalConnection { } ///Resize the terminal and the PTY. This locks the terminal. - pub fn set_size(&mut self, new_size: SizeInfo) { + pub fn set_size(&self, new_size: SizeInfo) { self.pty_tx.0.send(Msg::Resize(new_size)).ok(); self.term.lock().resize(new_size); } @@ -257,6 +259,12 @@ impl TerminalConnection { self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); } } + + // pub fn click(&mut self, pos: Vector2F, clicks: usize) {} + + // pub fn drag(prev_pos: Vector2F, pos: Vector2F) {} + + // pub fn mouse_down(pos: Vector2F) {} } impl Drop for TerminalConnection { diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index f69827b34e..48e1883729 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -1,13 +1,14 @@ +mod terminal_layout_context; + use alacritty_terminal::{ grid::{Dimensions, GridIterator, Indexed, Scroll}, index::{Column as GridCol, Line as GridLine, Point, Side}, selection::{Selection, SelectionRange, SelectionType}, - sync::FairMutex, term::{ cell::{Cell, Flags}, SizeInfo, }, - Term, + Grid, }; use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ @@ -30,25 +31,18 @@ use settings::Settings; use theme::TerminalStyle; use util::ResultExt; -use std::{cmp::min, ops::Range, sync::Arc}; +use std::{cmp::min, ops::Range}; use std::{fmt::Debug, ops::Sub}; -use crate::{ - color_translation::convert_color, - connection::{TerminalConnection, ZedListener}, - Terminal, -}; +use crate::{color_translation::convert_color, connection::TerminalConnection, Terminal}; + +use self::terminal_layout_context::TerminalLayoutContext; ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I ///Implement scroll bars. const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; -///Used to display the grid as passed to Alacritty and the TTY. -///Useful for debugging inconsistencies between behavior and display -#[cfg(debug_assertions)] -const DEBUG_GRID: bool = false; - ///The GPUI element that paints the terminal. ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalEl { @@ -58,8 +52,8 @@ pub struct TerminalEl { } ///New type pattern so I don't mix these two up -struct CellWidth(f32); -struct LineHeight(f32); +pub struct CellWidth(f32); +pub struct LineHeight(f32); struct LayoutLine { cells: Vec, @@ -98,8 +92,6 @@ pub struct LayoutState { em_width: CellWidth, cursor: Option, background_color: Color, - cur_size: SizeInfo, - terminal: Arc>>, selection_color: Color, } @@ -115,6 +107,102 @@ impl TerminalEl { modal, } } + + fn attach_mouse_handlers( + &self, + origin: Vector2F, + view_id: usize, + visible_bounds: RectF, + cx: &mut PaintContext, + ) { + let mouse_down_connection = self.connection.clone(); + let click_connection = self.connection.clone(); + let drag_connection = self.connection.clone(); + cx.scene.push_mouse_region( + MouseRegion::new(view_id, None, visible_bounds) + .on_down( + MouseButton::Left, + move |MouseButtonEvent { position, .. }, cx| { + if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) { + conn_handle.update(cx.app, |conn, _cx| { + let mut term = conn.term.lock(); + let (point, side) = mouse_to_cell_data( + position, + origin, + conn.cur_size, + term.renderable_content().display_offset, + ); + term.selection = + Some(Selection::new(SelectionType::Simple, point, side)) + }); + } + }, + ) + .on_click( + MouseButton::Left, + move |MouseButtonEvent { + position, + click_count, + .. + }, + cx| { + cx.focus_parent_view(); + if let Some(conn_handle) = click_connection.upgrade(cx.app) { + conn_handle.update(cx.app, |conn, cx| { + let mut term = conn.term.lock(); + + let (point, side) = mouse_to_cell_data( + position, + origin, + conn.cur_size, + term.renderable_content().display_offset, + ); + + let selection_type = match click_count { + 0 => return, //This is a release + 1 => Some(SelectionType::Simple), + 2 => Some(SelectionType::Semantic), + 3 => Some(SelectionType::Lines), + _ => None, + }; + + let selection = selection_type.map(|selection_type| { + Selection::new(selection_type, point, side) + }); + + term.selection = selection; + + cx.notify(); + }); + } + }, + ) + .on_drag( + MouseButton::Left, + move |_, MouseMovedEvent { position, .. }, cx| { + if let Some(conn_handle) = drag_connection.upgrade(cx.app) { + conn_handle.update(cx.app, |conn, cx| { + let mut term = conn.term.lock(); + + let (point, side) = mouse_to_cell_data( + position, + origin, + conn.cur_size, + term.renderable_content().display_offset, + ); + + if let Some(mut selection) = term.selection.take() { + selection.update(point, side); + term.selection = Some(selection); + } + + cx.notify() + }); + } + }, + ), + ); + } } impl Element for TerminalEl { @@ -126,101 +214,65 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - //Settings immutably borrows cx here for the settings and font cache - //and we need to modify the cx to resize the terminal. So instead of - //storing Settings or the font_cache(), we toss them ASAP and then reborrow later - let text_style = make_text_style(cx.font_cache(), cx.global::()); - let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size)); - let cell_width = CellWidth( - cx.font_cache() - .em_advance(text_style.font_id, text_style.font_size), - ); - let connection_handle = self.connection.upgrade(cx).unwrap(); + let tcx = TerminalLayoutContext::new(cx.global::(), &cx.font_cache()); - //Tell the view our new size. Requires a mutable borrow of cx and the view - let cur_size = make_new_size(constraint, &cell_width, &line_height); - //Note that set_size locks and mutates the terminal. - connection_handle.update(cx.app, |connection, _| connection.set_size(cur_size)); - - let (selection_color, terminal_theme) = { - let theme = &(cx.global::()).theme; - (theme.editor.selection.selection, &theme.terminal) + let term = { + let connection = self.connection.upgrade(cx).unwrap().read(cx); + //This locks the terminal, so resize it first. + connection.set_size(make_new_size(constraint, &tcx.cell_width, &tcx.line_height)); + connection.term.lock() }; - let terminal_mutex = connection_handle.read(cx).term.clone(); - let term = terminal_mutex.lock(); - let grid = term.grid(); - let cursor_point = grid.cursor.point; - let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string(); - let content = term.renderable_content(); + /* + * TODO for layouts: + * - Refactor this whole process to produce 'text cells', 'background rects', and 'selections' which know + * how to paint themselves + * - Rather than doing everything per cell, map each cell into a tuple and then unzip the streams + * - For efficiency: + * - filter out all background colored background rects + * - filter out all text cells which just contain ' ' + * - Smoosh together rectangles on same line + + */ + //Layout grid cells let layout_lines = layout_lines( content.display_iter, - &text_style, - terminal_theme, + &tcx.text_style, + tcx.terminal_theme, cx.text_layout_cache, self.modal, content.selection, ); - let block_text = cx.text_layout_cache.layout_str( - &cursor_text, - text_style.font_size, - &[( - cursor_text.len(), - RunStyle { - font_id: text_style.font_id, - color: terminal_theme.colors.background, - underline: Default::default(), - }, - )], + //Layout cursor + let cursor = layout_cursor( + term.grid(), + cx.text_layout_cache, + &tcx, + content.cursor.point, + content.display_offset, + constraint, ); - let cursor = get_cursor_shape( - content.cursor.point.line.0 as usize, - content.cursor.point.column.0 as usize, - content.display_offset, - &line_height, - &cell_width, - cur_size.total_lines(), - &block_text, - ) - .map(move |(cursor_position, block_width)| { - let block_width = if block_width != 0.0 { - block_width - } else { - cell_width.0 - }; - - Cursor::new( - cursor_position, - block_width, - line_height.0, - terminal_theme.colors.cursor, - CursorShape::Block, - Some(block_text.clone()), - ) - }); - drop(term); - + //Select background color let background_color = if self.modal { - terminal_theme.colors.modal_background + tcx.terminal_theme.colors.modal_background } else { - terminal_theme.colors.background + tcx.terminal_theme.colors.background }; + //Done! ( constraint.max, LayoutState { layout_lines, - line_height, - em_width: cell_width, + line_height: tcx.line_height, + em_width: tcx.cell_width, cursor, - cur_size, background_color, - terminal: terminal_mutex, - selection_color, + selection_color: tcx.selection_color, }, ) } @@ -232,22 +284,21 @@ impl Element for TerminalEl { layout: &mut Self::LayoutState, cx: &mut gpui::PaintContext, ) -> Self::PaintState { + /* + * For paint, I want to change how mouse events are handled: + * - Refactor the mouse handlers to push the grid cell actions into the connection + * - But keep the conversion from GPUI coordinates to grid cells in the Terminal element + * - Switch from directly painting things, to calling 'paint' on items produced by layout + */ + //Setup element stuff let clip_bounds = Some(visible_bounds); cx.paint_layer(clip_bounds, |cx| { - let cur_size = layout.cur_size.clone(); let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse - attach_mouse_handlers( - origin, - cur_size, - self.view.id(), - &layout.terminal, - visible_bounds, - cx, - ); + self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, cx); cx.paint_layer(clip_bounds, |cx| { //Start with a background color @@ -345,13 +396,6 @@ impl Element for TerminalEl { cursor.paint(origin, cx); }) } - - #[cfg(debug_assertions)] - if DEBUG_GRID { - cx.paint_layer(clip_bounds, |cx| { - draw_debug_grid(bounds, layout, cx); - }) - } }); } @@ -418,6 +462,64 @@ impl Element for TerminalEl { } } +fn layout_cursor( + grid: &Grid, + text_layout_cache: &TextLayoutCache, + tcx: &TerminalLayoutContext, + cursor_point: Point, + display_offset: usize, + constraint: SizeConstraint, +) -> Option { + let cursor_text = layout_cursor_text(grid, text_layout_cache, tcx); + get_cursor_shape( + cursor_point.line.0 as usize, + cursor_point.column.0 as usize, + display_offset, + &tcx.line_height, + &tcx.cell_width, + (constraint.max.y() / &tcx.line_height.0) as usize, //TODO + &cursor_text, + ) + .map(move |(cursor_position, block_width)| { + let block_width = if block_width != 0.0 { + block_width + } else { + tcx.cell_width.0 + }; + + Cursor::new( + cursor_position, + block_width, + tcx.line_height.0, + tcx.terminal_theme.colors.cursor, + CursorShape::Block, + Some(cursor_text.clone()), + ) + }) +} + +fn layout_cursor_text( + grid: &Grid, + text_layout_cache: &TextLayoutCache, + tcx: &TerminalLayoutContext, +) -> Line { + let cursor_point = grid.cursor.point; + let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string(); + + text_layout_cache.layout_str( + &cursor_text, + tcx.text_style.font_size, + &[( + cursor_text.len(), + RunStyle { + font_id: tcx.text_style.font_id, + color: tcx.terminal_theme.colors.background, + underline: Default::default(), + }, + )], + ) +} + pub fn mouse_to_cell_data( pos: Vector2F, origin: Vector2F, @@ -430,40 +532,6 @@ pub fn mouse_to_cell_data( (point, side) } -///Configures a text style from the current settings. -fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { - // Pull the font family from settings properly overriding - let family_id = settings - .terminal_overrides - .font_family - .as_ref() - .and_then(|family_name| font_cache.load_family(&[family_name]).log_err()) - .or_else(|| { - settings - .terminal_defaults - .font_family - .as_ref() - .and_then(|family_name| font_cache.load_family(&[family_name]).log_err()) - }) - .unwrap_or(settings.buffer_font_family); - - TextStyle { - color: settings.theme.editor.text_color, - font_family_id: family_id, - font_family_name: font_cache.family_name(family_id).unwrap(), - font_id: font_cache - .select_font(family_id, &Default::default()) - .unwrap(), - font_size: settings - .terminal_overrides - .font_size - .or(settings.terminal_defaults.font_size) - .unwrap_or(settings.buffer_font_size), - font_properties: Default::default(), - underline: Default::default(), - } -} - ///Configures a size info object from the given information. fn make_new_size( constraint: SizeConstraint, @@ -592,89 +660,89 @@ fn cell_style( } } -fn attach_mouse_handlers( - origin: Vector2F, - cur_size: SizeInfo, - view_id: usize, - terminal_mutex: &Arc>>, - visible_bounds: RectF, - cx: &mut PaintContext, -) { - let click_mutex = terminal_mutex.clone(); - let drag_mutex = terminal_mutex.clone(); - let mouse_down_mutex = terminal_mutex.clone(); +// fn attach_mouse_handlers( +// origin: Vector2F, +// cur_size: SizeInfo, +// view_id: usize, +// terminal_mutex: &Arc>>, +// visible_bounds: RectF, +// cx: &mut PaintContext, +// ) { +// let click_mutex = terminal_mutex.clone(); +// let drag_mutex = terminal_mutex.clone(); +// let mouse_down_mutex = terminal_mutex.clone(); - cx.scene.push_mouse_region( - MouseRegion::new(view_id, None, visible_bounds) - .on_down( - MouseButton::Left, - move |MouseButtonEvent { position, .. }, _| { - let mut term = mouse_down_mutex.lock(); +// cx.scene.push_mouse_region( +// MouseRegion::new(view_id, None, visible_bounds) +// .on_down( +// MouseButton::Left, +// move |MouseButtonEvent { position, .. }, _| { +// let mut term = mouse_down_mutex.lock(); - let (point, side) = mouse_to_cell_data( - position, - origin, - cur_size, - term.renderable_content().display_offset, - ); - term.selection = Some(Selection::new(SelectionType::Simple, point, side)) - }, - ) - .on_click( - MouseButton::Left, - move |MouseButtonEvent { - position, - click_count, - .. - }, - cx| { - let mut term = click_mutex.lock(); +// let (point, side) = mouse_to_cell_data( +// position, +// origin, +// cur_size, +// term.renderable_content().display_offset, +// ); +// term.selection = Some(Selection::new(SelectionType::Simple, point, side)) +// }, +// ) +// .on_click( +// MouseButton::Left, +// move |MouseButtonEvent { +// position, +// click_count, +// .. +// }, +// cx| { +// let mut term = click_mutex.lock(); - let (point, side) = mouse_to_cell_data( - position, - origin, - cur_size, - term.renderable_content().display_offset, - ); +// let (point, side) = mouse_to_cell_data( +// position, +// origin, +// cur_size, +// term.renderable_content().display_offset, +// ); - let selection_type = match click_count { - 0 => return, //This is a release - 1 => Some(SelectionType::Simple), - 2 => Some(SelectionType::Semantic), - 3 => Some(SelectionType::Lines), - _ => None, - }; +// let selection_type = match click_count { +// 0 => return, //This is a release +// 1 => Some(SelectionType::Simple), +// 2 => Some(SelectionType::Semantic), +// 3 => Some(SelectionType::Lines), +// _ => None, +// }; - let selection = selection_type - .map(|selection_type| Selection::new(selection_type, point, side)); +// let selection = selection_type +// .map(|selection_type| Selection::new(selection_type, point, side)); - term.selection = selection; - cx.focus_parent_view(); - cx.notify(); - }, - ) - .on_drag( - MouseButton::Left, - move |_, MouseMovedEvent { position, .. }, cx| { - let mut term = drag_mutex.lock(); +// term.selection = selection; +// cx.focus_parent_view(); +// cx.notify(); +// }, +// ) +// .on_drag( +// MouseButton::Left, +// move |_, MouseMovedEvent { position, .. }, cx| { +// let mut term = drag_mutex.lock(); - let (point, side) = mouse_to_cell_data( - position, - origin, - cur_size, - term.renderable_content().display_offset, - ); +// let (point, side) = mouse_to_cell_data( +// position, +// origin, +// cur_size, +// term.renderable_content().display_offset, +// ); - if let Some(mut selection) = term.selection.take() { - selection.update(point, side); - term.selection = Some(selection); - } +// if let Some(mut selection) = term.selection.take() { +// selection.update(point, side); +// term.selection = Some(selection); +// } - cx.notify(); - }, - ), - ); -} +// cx.notify(); +// }, +// ), +// ); +// } ///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side { @@ -714,37 +782,6 @@ fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) - Point::new(GridLine(line - display_offset as i32), col) } -///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between -///Display and conceptual grid. -#[cfg(debug_assertions)] -fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { - let width = layout.cur_size.width(); - let height = layout.cur_size.height(); - //Alacritty uses 'as usize', so shall we. - for col in 0..(width / layout.em_width.0).round() as usize { - cx.scene.push_quad(Quad { - bounds: RectF::new( - bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.), - vec2f(1., height), - ), - background: Some(Color::green()), - border: Default::default(), - corner_radius: 0., - }); - } - for row in 0..((height / layout.line_height.0) + 1.0).round() as usize { - cx.scene.push_quad(Quad { - bounds: RectF::new( - bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0), - vec2f(width, 1.), - ), - background: Some(Color::green()), - border: Default::default(), - corner_radius: 0., - }); - } -} - mod test { #[test] diff --git a/crates/terminal/src/terminal_element/terminal_layout_context.rs b/crates/terminal/src/terminal_element/terminal_layout_context.rs new file mode 100644 index 0000000000..5ddbbdec7b --- /dev/null +++ b/crates/terminal/src/terminal_element/terminal_layout_context.rs @@ -0,0 +1,59 @@ +use super::*; + +pub struct TerminalLayoutContext<'a> { + pub line_height: LineHeight, + pub cell_width: CellWidth, + pub text_style: TextStyle, + pub selection_color: Color, + pub terminal_theme: &'a TerminalStyle, +} + +impl<'a> TerminalLayoutContext<'a> { + pub fn new(settings: &'a Settings, font_cache: &FontCache) -> Self { + let text_style = Self::make_text_style(font_cache, &settings); + let line_height = LineHeight(font_cache.line_height(text_style.font_size)); + let cell_width = CellWidth(font_cache.em_advance(text_style.font_id, text_style.font_size)); + let selection_color = settings.theme.editor.selection.selection; + let terminal_theme = &settings.theme.terminal; + + TerminalLayoutContext { + line_height, + cell_width, + text_style, + selection_color, + terminal_theme, + } + } + + ///Configures a text style from the current settings. + fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { + // Pull the font family from settings properly overriding + let family_id = settings + .terminal_overrides + .font_family + .as_ref() + .or_else(|| settings.terminal_defaults.font_family.as_ref()) + .and_then(|family_name| font_cache.load_family(&[family_name]).log_err()) + .unwrap_or(settings.buffer_font_family); + + let font_size = settings + .terminal_overrides + .font_size + .or(settings.terminal_defaults.font_size) + .unwrap_or(settings.buffer_font_size); + + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + + TextStyle { + color: settings.theme.editor.text_color, + font_family_id: family_id, + font_family_name: font_cache.family_name(family_id).unwrap(), + font_id, + font_size, + font_properties: Default::default(), + underline: Default::default(), + } + } +} From 9d063ae6d8352d249ade698d6617fdebaf186415 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 16 Jul 2022 22:17:20 -0700 Subject: [PATCH 04/29] Switched to hybrid iterator and while loop grid processor. Still hairy but much more managable. Not finished compiling yet. --- crates/terminal/src/terminal_element.rs | 215 ++++++++++++++++-------- 1 file changed, 143 insertions(+), 72 deletions(-) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 48e1883729..514d5ee1d4 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -1,6 +1,7 @@ mod terminal_layout_context; use alacritty_terminal::{ + ansi::{Color::Named, NamedColor}, grid::{Dimensions, GridIterator, Indexed, Scroll}, index::{Column as GridCol, Line as GridLine, Point, Side}, selection::{Selection, SelectionRange, SelectionType}, @@ -55,11 +56,6 @@ pub struct TerminalEl { pub struct CellWidth(f32); pub struct LineHeight(f32); -struct LayoutLine { - cells: Vec, - highlighted_range: Option>, -} - ///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than struct PaneRelativePos(Vector2F); @@ -68,26 +64,11 @@ fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos { PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating } -#[derive(Clone, Debug, Default)] -struct LayoutCell { - point: Point, - text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN! - background_color: Color, -} - -impl LayoutCell { - fn new(point: Point, text: Line, background_color: Color) -> LayoutCell { - LayoutCell { - point, - text, - background_color, - } - } -} - ///The information generated during layout that is nescessary for painting pub struct LayoutState { - layout_lines: Vec, + cells: Vec, + rects: Vec, + highlights: Vec, line_height: LineHeight, em_width: CellWidth, cursor: Option, @@ -225,19 +206,9 @@ impl Element for TerminalEl { let content = term.renderable_content(); - /* - * TODO for layouts: - * - Refactor this whole process to produce 'text cells', 'background rects', and 'selections' which know - * how to paint themselves - * - Rather than doing everything per cell, map each cell into a tuple and then unzip the streams - * - For efficiency: - * - filter out all background colored background rects - * - filter out all text cells which just contain ' ' - * - Smoosh together rectangles on same line - - */ //Layout grid cells - let layout_lines = layout_lines( + + let (cells, rects, highlights) = layout_grid( content.display_iter, &tcx.text_style, tcx.terminal_theme, @@ -267,12 +238,14 @@ impl Element for TerminalEl { ( constraint.max, LayoutState { - layout_lines, line_height: tcx.line_height, em_width: tcx.cell_width, cursor, background_color, selection_color: tcx.selection_color, + cells, + rects, + highlights, }, ) } @@ -310,7 +283,7 @@ impl Element for TerminalEl { }); //Draw cell backgrounds - for layout_line in &layout.layout_lines { + for layout_line in &layout.layout_cells { for layout_cell in &layout_line.cells { let position = vec2f( (origin.x() + layout_cell.point.column as f32 * layout.em_width.0) @@ -333,7 +306,7 @@ impl Element for TerminalEl { cx.paint_layer(clip_bounds, |cx| { let mut highlight_y = None; let highlight_lines = layout - .layout_lines + .layout_cells .iter() .filter_map(|line| { if let Some(range) = &line.highlighted_range { @@ -370,7 +343,7 @@ impl Element for TerminalEl { }); cx.paint_layer(clip_bounds, |cx| { - for layout_line in &layout.layout_lines { + for layout_line in &layout.layout_cells { for layout_cell in &layout_line.cells { let point = layout_cell.point; @@ -549,57 +522,155 @@ fn make_new_size( ) } -fn layout_lines( +#[derive(Clone, Debug, Default)] +struct LayoutCell { + point: Point, + text: Line, +} + +impl LayoutCell { + fn new(point: Point, text: Line) -> LayoutCell { + LayoutCell { point, text } + } +} + +#[derive(Clone, Debug, Default)] +struct LayoutRect { + pos: Point, + num_of_cells: usize, + color: Color, +} + +impl LayoutRect { + fn new(pos: Point, num_of_cells: usize, color: Color) -> LayoutRect { + LayoutRect { + pos, + num_of_cells, + color, + } + } + + fn extend(&mut self) { + self.num_of_cells += 1; + } +} + +struct RelativeHighlightedRange { + line_index: usize, + range: Range, +} + +impl RelativeHighlightedRange { + fn new(line_index: usize, range: Range) -> Self { + RelativeHighlightedRange { line_index, range } + } +} + +fn layout_grid( grid: GridIterator, text_style: &TextStyle, terminal_theme: &TerminalStyle, text_layout_cache: &TextLayoutCache, modal: bool, selection_range: Option, -) -> Vec { - let lines = grid.group_by(|i| i.point.line); - lines - .into_iter() - .enumerate() - .map(|(line_index, (_, line))| { - let mut highlighted_range = None; - let cells = line - .enumerate() - .map(|(x_index, indexed_cell)| { - if selection_range - .map(|range| range.contains(indexed_cell.point)) - .unwrap_or(false) - { - let mut range = highlighted_range.take().unwrap_or(x_index..x_index); - range.end = range.end.max(x_index); - highlighted_range = Some(range); +) -> ( + Vec, + Vec, + Vec, +) { + let mut cells = vec![]; + let mut rects = vec![]; + let mut highlight_ranges = vec![]; + + let mut cur_rect: Option = None; + let mut cur_alac_color = None; + let mut highlighted_range = None; + + let linegroups = grid.group_by(|i| i.point.line); + for (line_index, (_, line)) in linegroups.into_iter().enumerate() { + for (x_index, cell) in line.enumerate() { + //Increase selection range + { + if selection_range + .map(|range| range.contains(cell.point)) + .unwrap_or(false) + { + let mut range = highlighted_range.take().unwrap_or(x_index..x_index); + range.end = range.end.max(x_index); + highlighted_range = Some(range); + } + } + + //Expand background rect range + { + match (cell.bg, cur_alac_color) { + (Named(NamedColor::Background), Some(_)) => { + //Skip color, end background + cur_alac_color = None; + rects.push(cur_rect.take().unwrap()); } + (bg, Some(cur_color)) => { + //If they're the same, extend the match + if bg == cur_color { + cur_rect.unwrap().extend() + } else { + //If they differ, end background and restart + cur_alac_color = None; + rects.push(cur_rect.take().unwrap()); - let cell_text = &indexed_cell.c.to_string(); + cur_alac_color = Some(bg); + cur_rect = Some(LayoutRect::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + 1, + convert_color(&bg, &terminal_theme.colors, modal), + )); + } + } + (bg, None) if !matches!(bg, Named(NamedColor::Background)) => { + //install new background + cur_alac_color = Some(bg); + cur_rect = Some(LayoutRect::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + 1, + convert_color(&bg, &terminal_theme.colors, modal), + )); + } + (_, _) => {} //Only happens when bg is NamedColor::Background + } + } - let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal); + //Layout current cell text + { + let cell_text = &cell.c.to_string(); + if cell_text != " " { + let cell_style = cell_style(&cell, terminal_theme, text_style, modal); - //This is where we might be able to get better performance let layout_cell = text_layout_cache.layout_str( cell_text, text_style.font_size, &[(cell_text.len(), cell_style)], ); - LayoutCell::new( - Point::new(line_index as i32, indexed_cell.point.column.0 as i32), + cells.push(LayoutCell::new( + Point::new(line_index as i32, cell.point.column.0 as i32), layout_cell, - convert_color(&indexed_cell.bg, &terminal_theme.colors, modal), - ) - }) - .collect::>(); + )) + } + }; + } - LayoutLine { - cells, - highlighted_range, - } - }) - .collect::>() + if highlighted_range.is_some() { + highlight_ranges.push(RelativeHighlightedRange::new( + line_index, + highlighted_range.take().unwrap(), + )) + } + + if cur_rect.is_some() { + rects.push(cur_rect.take().unwrap()); + } + } + (cells, rects, highlight_ranges) } // Compute the cursor position and expected block width, may return a zero width if x_for_index returns From 40d30a898bcfe1a9d897174967bd188d1b94f779 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 18 Jul 2022 14:50:33 -0700 Subject: [PATCH 05/29] Rebasing continues --- Cargo.lock | 1 + crates/gpui/src/app.rs | 15 ++++++++ crates/settings/src/settings.rs | 2 +- crates/terminal/Cargo.toml | 1 + crates/terminal/src/connection.rs | 37 +++++-------------- crates/terminal/src/modal.rs | 7 +++- crates/terminal/src/terminal.rs | 26 ++++++++----- .../src/tests/terminal_test_context.rs | 2 +- 8 files changed, 52 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 184eb355a1..0068911060 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5355,6 +5355,7 @@ name = "terminal" version = "0.1.0" dependencies = [ "alacritty_terminal", + "anyhow", "client", "dirs 4.0.0", "editor", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2cd6687bcb..db6fe6cdd2 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1781,6 +1781,21 @@ impl MutableAppContext { }) } + pub fn try_add_model(&mut self, build_model: F) -> Result> + where + T: Entity, + F: FnOnce(&mut ModelContext) -> Result, + { + self.update(|this| { + let model_id = post_inc(&mut this.next_entity_id); + let handle = ModelHandle::new(model_id, &this.cx.ref_counts); + let mut cx = ModelContext::new(this, model_id); + let model = build_model(&mut cx)?; + this.cx.models.insert(model_id, Box::new(model)); + Ok(handle) + }) + } + pub fn add_window( &mut self, window_options: WindowOptions, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 76cc653ff8..c518888456 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -81,7 +81,7 @@ pub struct TerminalSettings { pub working_directory: Option, pub font_size: Option, pub font_family: Option, - pub env: Option>, + pub env: Option>, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 6c5db4c91b..c81be6c524 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -24,6 +24,7 @@ itertools = "0.10" dirs = "4.0.0" shellexpand = "2.1.0" libc = "0.2" +anyhow = "1" [dev-dependencies] diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index c9aa574c01..6515806949 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -11,9 +11,10 @@ use alacritty_terminal::{ tty::{self, setup_env}, Term, }; -use futures::{ - channel::mpsc::{unbounded, UnboundedSender}, - StreamExt, +use anyhow::Result; +use futures::channel::mpsc::{ + unbounded::{self, UndboundedSender}, + UnboundedSender, }; use settings::{Settings, Shell}; use std::{collections::HashMap, path::PathBuf, sync::Arc}; @@ -59,10 +60,10 @@ impl TerminalConnection { pub fn new( working_directory: Option, shell: Option, - env_vars: Option>, + env: Option>, initial_size: SizeInfo, cx: &mut ModelContext, - ) -> TerminalConnection { + ) -> Result { let pty_config = { let shell = shell.and_then(|shell| match shell { Shell::System => None, @@ -77,12 +78,7 @@ impl TerminalConnection { } }; - let mut env: HashMap = HashMap::new(); - if let Some(envs) = env_vars { - for (var, val) in envs { - env.insert(var, val); - } - } + let mut env = env.unwrap_or_else(|| HashMap::new()); //TODO: Properly set the current locale, env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); @@ -103,20 +99,7 @@ impl TerminalConnection { let term = Arc::new(FairMutex::new(term)); //Setup the pty... - let pty = { - if let Some(pty) = tty::new(&pty_config, &initial_size, None).ok() { - pty - } else { - let pty_config = PtyConfig { - shell: None, - working_directory: working_directory.clone(), - ..Default::default() - }; - - tty::new(&pty_config, &initial_size, None) - .expect("Failed with default shell too :(") - } - }; + let pty = tty::new(&pty_config, &initial_size, None)?; let shell = { let mut buf = [0; 1024]; @@ -154,13 +137,13 @@ impl TerminalConnection { }) .detach(); - TerminalConnection { + Ok(TerminalConnection { pty_tx: Notifier(pty_tx), term, title: shell.to_string(), cur_size: initial_size, associated_directory: working_directory, - } + }) } ///Takes events from Alacritty and translates them to behavior on this view diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 708f96856b..9d8ae6330b 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -25,7 +25,12 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon // No connection was stored, create a new terminal if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { let wd = get_wd_for_workspace(workspace, cx); - let this = cx.add_view(|cx| Terminal::new(wd, true, cx)); + + //TODO: Anything other than crash. + let this = cx + .add_option_view(|cx| Terminal::new(wd, true, cx).ok()) + .unwrap(); + let connection_handle = this.read(cx).connection.clone(); cx.subscribe(&connection_handle, on_event).detach(); //Set the global immediately, in case the user opens the command palette diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 1f25289270..07a029bd8e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -4,7 +4,7 @@ mod modal; pub mod terminal_element; use alacritty_terminal::term::SizeInfo; - +use anyhow::Result; use connection::{Event, TerminalConnection}; use dirs::home_dir; use gpui::{ @@ -79,7 +79,11 @@ impl Entity for Terminal { impl Terminal { ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` - fn new(working_directory: Option, modal: bool, cx: &mut ViewContext) -> Self { + fn new( + working_directory: Option, + modal: bool, + cx: &mut ViewContext, + ) -> Result { //The details here don't matter, the terminal will be resized on the first layout let size_info = SizeInfo::new( DEBUG_TERMINAL_WIDTH, @@ -98,10 +102,11 @@ impl Terminal { (shell, envs) }; - let connection = cx - .add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx)); + let connection = cx.try_add_model(|cx| { + TerminalConnection::new(working_directory, shell, envs, size_info, cx) + })?; - Terminal::from_connection(connection, modal, cx) + Ok(Terminal::from_connection(connection, modal, cx)) } fn from_connection( @@ -152,7 +157,9 @@ impl Terminal { ///Create a new Terminal in the current working directory or the user's home directory fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let wd = get_wd_for_workspace(workspace, cx); - workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(wd, false, cx))), cx); + if let Some(view) = cx.add_option_view(|cx| Terminal::new(wd, false, cx).ok()) { + workspace.add_item(Box::new(view), cx); + } } ///Attempt to paste the clipboard into the terminal @@ -266,13 +273,14 @@ impl Item for Terminal { fn clone_on_split(&self, cx: &mut ViewContext) -> Option { //From what I can tell, there's no way to tell the current working - //Directory of the terminal from outside the terminal. There might be + //Directory of the terminal from outside the shell. There might be //solutions to this, but they are non-trivial and require more IPC - Some(Terminal::new( + Terminal::new( self.connection.read(cx).associated_directory.clone(), false, cx, - )) + ) + .ok() } fn project_path(&self, _cx: &gpui::AppContext) -> Option { diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index b5696aff13..64d602ffb5 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -29,7 +29,7 @@ impl<'a> TerminalTestContext<'a> { ); let connection = - cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx)); + cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx).unwrap()); TerminalTestContext { cx, connection } } From 4a483618bee35963f6b64388ecb843f9e3c82fc2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 16 Jul 2022 10:34:47 -0700 Subject: [PATCH 06/29] Finished graceful terminal failure --- assets/settings/default.json | 8 ++++---- crates/gpui/src/app.rs | 11 +++++++---- crates/terminal/src/modal.rs | 5 +++-- crates/terminal/src/terminal.rs | 3 ++- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 73c73636f6..6c34d6be70 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -102,10 +102,10 @@ // "working_directory": "current_project_directory", //Any key-value pairs added to this list will be added to the terminal's - //enviroment. Use `:` to seperate multiple values, not multiple list items - "env": [ - //["KEY", "value1:value2"] - ] + //enviroment. Use `:` to seperate multiple values. + "env": { + //"KEY": "value1:value2" + } //Set the terminal's font size. If this option is not included, //the terminal will default to matching the buffer's font size. //"font_size": "15" diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index db6fe6cdd2..547e55699c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1970,10 +1970,13 @@ impl MutableAppContext { for model_id in dropped_models { self.subscriptions.lock().remove(&model_id); self.observations.lock().remove(&model_id); - let mut model = self.cx.models.remove(&model_id).unwrap(); - model.release(self); - self.pending_effects - .push_back(Effect::ModelRelease { model_id, model }); + //Model handles and IDs may have been created to instantiate a model without + //finishing successfully (`try_add_model()`) + if let Some(mut model) = self.cx.models.remove(&model_id) { + model.release(self); + self.pending_effects + .push_back(Effect::ModelRelease { model_id, model }); + } } for (window_id, view_id) in dropped_views { diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 9d8ae6330b..7349ae233e 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -1,4 +1,5 @@ use gpui::{ModelHandle, ViewContext}; +use util::ResultExt; use workspace::Workspace; use crate::{get_wd_for_workspace, DeployModal, Event, Terminal, TerminalConnection}; @@ -26,9 +27,9 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { let wd = get_wd_for_workspace(workspace, cx); - //TODO: Anything other than crash. + //TODO: Create a 'failed to launch' view which prints the error and config details. let this = cx - .add_option_view(|cx| Terminal::new(wd, true, cx).ok()) + .add_option_view(|cx| Terminal::new(wd, true, cx).log_err()) .unwrap(); let connection_handle = this.read(cx).connection.clone(); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 07a029bd8e..605d9533f2 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -17,6 +17,7 @@ use project::{LocalWorktree, Project, ProjectPath}; use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::path::{Path, PathBuf}; +use util::ResultExt; use workspace::{Item, Workspace}; use crate::terminal_element::TerminalEl; @@ -157,7 +158,7 @@ impl Terminal { ///Create a new Terminal in the current working directory or the user's home directory fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let wd = get_wd_for_workspace(workspace, cx); - if let Some(view) = cx.add_option_view(|cx| Terminal::new(wd, false, cx).ok()) { + if let Some(view) = cx.add_option_view(|cx| Terminal::new(wd, false, cx).log_err()) { workspace.add_item(Box::new(view), cx); } } From 150d2ff53fb021be7ed82316a5e96fe948565910 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 18 Jul 2022 14:53:56 -0700 Subject: [PATCH 07/29] Attempting to continue rebase --- crates/gpui/src/app.rs | 26 +--- crates/terminal/src/connection.rs | 109 ++++++++++--- crates/terminal/src/modal.rs | 12 +- crates/terminal/src/terminal.rs | 147 +++++++++--------- crates/terminal/src/terminal_element.rs | 103 ++++++------ .../src/tests/terminal_test_context.rs | 14 +- 6 files changed, 227 insertions(+), 184 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 547e55699c..2cd6687bcb 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1781,21 +1781,6 @@ impl MutableAppContext { }) } - pub fn try_add_model(&mut self, build_model: F) -> Result> - where - T: Entity, - F: FnOnce(&mut ModelContext) -> Result, - { - self.update(|this| { - let model_id = post_inc(&mut this.next_entity_id); - let handle = ModelHandle::new(model_id, &this.cx.ref_counts); - let mut cx = ModelContext::new(this, model_id); - let model = build_model(&mut cx)?; - this.cx.models.insert(model_id, Box::new(model)); - Ok(handle) - }) - } - pub fn add_window( &mut self, window_options: WindowOptions, @@ -1970,13 +1955,10 @@ impl MutableAppContext { for model_id in dropped_models { self.subscriptions.lock().remove(&model_id); self.observations.lock().remove(&model_id); - //Model handles and IDs may have been created to instantiate a model without - //finishing successfully (`try_add_model()`) - if let Some(mut model) = self.cx.models.remove(&model_id) { - model.release(self); - self.pending_effects - .push_back(Effect::ModelRelease { model_id, model }); - } + let mut model = self.cx.models.remove(&model_id).unwrap(); + model.release(self); + self.pending_effects + .push_back(Effect::ModelRelease { model_id, model }); } for (window_id, view_id) in dropped_views { diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index 6515806949..5809f11eaa 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -6,10 +6,11 @@ use alacritty_terminal::{ event::{Event as AlacTermEvent, EventListener, Notify}, event_loop::{EventLoop, Msg, Notifier}, grid::Scroll, + selection::Selection, sync::FairMutex, - term::{SizeInfo, TermMode}, + term::{cell::Cell, RenderableContent, SizeInfo, TermMode}, tty::{self, setup_env}, - Term, + Grid, Term, }; use anyhow::Result; use futures::channel::mpsc::{ @@ -48,12 +49,13 @@ impl EventListener for ZedListener { } } -pub struct TerminalConnection { - pub pty_tx: Notifier, - pub term: Arc>>, - pub title: String, - pub associated_directory: Option, - pub cur_size: SizeInfo, +pub enum TerminalConnection { + Connected(Terminal), + Disconnected { + directory: Option, + shell: Option, + error: std::io::Error, + }, } impl TerminalConnection { @@ -63,7 +65,7 @@ impl TerminalConnection { env: Option>, initial_size: SizeInfo, cx: &mut ModelContext, - ) -> Result { + ) -> TerminalConnection { let pty_config = { let shell = shell.and_then(|shell| match shell { Shell::System => None, @@ -99,7 +101,16 @@ impl TerminalConnection { let term = Arc::new(FairMutex::new(term)); //Setup the pty... - let pty = tty::new(&pty_config, &initial_size, None)?; + let pty = match tty::new(&pty_config, &initial_size, None) { + Ok(pty) => pty, + Err(error) => { + return TerminalConnection::Disconnected { + directory: working_directory, + shell, + error, + }; + } + }; let shell = { let mut buf = [0; 1024]; @@ -121,13 +132,20 @@ impl TerminalConnection { let pty_tx = event_loop.channel(); let _io_thread = event_loop.spawn(); + let terminal = Terminal { + pty_tx: Notifier(pty_tx), + term, + title: DEFAULT_TITLE.to_string(), + associated_directory: working_directory, + }; + cx.spawn_weak(|this, mut cx| async move { //Listen for terminal events while let Some(event) = events_rx.next().await { match this.upgrade(&cx) { Some(this) => { this.update(&mut cx, |this, cx| { - this.process_terminal_event(event, cx); + terminal.process_terminal_event(event, cx); cx.notify(); }); } @@ -137,20 +155,30 @@ impl TerminalConnection { }) .detach(); - Ok(TerminalConnection { - pty_tx: Notifier(pty_tx), - term, - title: shell.to_string(), - cur_size: initial_size, - associated_directory: working_directory, - }) + TerminalConnection::Connected(terminal) } + pub fn get_terminal(&self) -> Option<&Terminal> { + match self { + TerminalConnection::Connected(conn) => Some(&conn), + TerminalConnection::Disconnected { .. } => None, + } + } +} + +pub struct Terminal { + pty_tx: Notifier, + term: Arc>>, + pub title: String, + pub associated_directory: Option, +} + +impl Terminal { ///Takes events from Alacritty and translates them to behavior on this view fn process_terminal_event( &mut self, event: alacritty_terminal::event::Event, - cx: &mut ModelContext, + cx: &mut ModelContext, ) { match event { // TODO: Handle is_self_focused in subscription on terminal view @@ -198,12 +226,12 @@ impl TerminalConnection { } ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - pub fn write_to_pty(&mut self, input: String) { + pub fn write_to_pty(&self, input: String) { self.write_bytes_to_pty(input.into_bytes()); } ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - fn write_bytes_to_pty(&mut self, input: Vec) { + fn write_bytes_to_pty(&self, input: Vec) { self.term.lock().scroll_display(Scroll::Bottom); self.pty_tx.notify(input); } @@ -214,12 +242,12 @@ impl TerminalConnection { self.term.lock().resize(new_size); } - pub fn clear(&mut self) { + pub fn clear(&self) { self.write_to_pty("\x0c".into()); self.term.lock().clear_screen(ClearMode::Saved); } - pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool { + pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool { let guard = self.term.lock(); let mode = guard.mode(); let esc = to_esc_str(keystroke, mode); @@ -233,7 +261,7 @@ impl TerminalConnection { } ///Paste text into the terminal - pub fn paste(&mut self, text: &str) { + pub fn paste(&self, text: &str) { if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) { self.write_to_pty("\x1b[200~".to_string()); self.write_to_pty(text.replace('\x1b', "").to_string()); @@ -243,6 +271,32 @@ impl TerminalConnection { } } + pub fn copy(&self) -> Option { + let term = self.term.lock(); + term.selection_to_string() + } + + ///Takes the selection out of the terminal + pub fn take_selection(&self) -> Option { + self.term.lock().selection.take() + } + + ///Sets the selection object on the terminal + pub fn set_selection(&self, sel: Option) { + self.term.lock().selection = sel; + } + + ///Get the relevant rendering values from the terminal + pub fn renderable_content(&self) -> (RenderableContent, &Grid) { + let term = self.term.lock(); + (term.renderable_content(), term.grid()) + } + + ///Scroll the terminal + pub fn scroll(&self, scroll: Scroll) { + self.term.lock().scroll_display(scroll) + } + // pub fn click(&mut self, pos: Vector2F, clicks: usize) {} // pub fn drag(prev_pos: Vector2F, pos: Vector2F) {} @@ -252,7 +306,12 @@ impl TerminalConnection { impl Drop for TerminalConnection { fn drop(&mut self) { - self.pty_tx.0.send(Msg::Shutdown).ok(); + match self { + TerminalConnection::Connected(conn) => { + conn.pty_tx.0.send(Msg::Shutdown).ok(); + } + _ => {} + }; } } diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 7349ae233e..91d2d9ff73 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -1,8 +1,7 @@ use gpui::{ModelHandle, ViewContext}; -use util::ResultExt; use workspace::Workspace; -use crate::{get_wd_for_workspace, DeployModal, Event, Terminal, TerminalConnection}; +use crate::{get_wd_for_workspace, DeployModal, Event, TerminalConnection, TerminalView}; #[derive(Debug)] struct StoredConnection(ModelHandle); @@ -17,7 +16,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon if let Some(StoredConnection(stored_connection)) = possible_connection { // Create a view from the stored connection workspace.toggle_modal(cx, |_, cx| { - cx.add_view(|cx| Terminal::from_connection(stored_connection.clone(), true, cx)) + cx.add_view(|cx| TerminalView::from_connection(stored_connection.clone(), true, cx)) }); cx.set_global::>(Some(StoredConnection( stored_connection.clone(), @@ -27,10 +26,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { let wd = get_wd_for_workspace(workspace, cx); - //TODO: Create a 'failed to launch' view which prints the error and config details. - let this = cx - .add_option_view(|cx| Terminal::new(wd, true, cx).log_err()) - .unwrap(); + let this = cx.add_view(|cx| TerminalView::new(wd, true, cx)); let connection_handle = this.read(cx).connection.clone(); cx.subscribe(&connection_handle, on_event).detach(); @@ -60,7 +56,7 @@ pub fn on_event( if workspace .modal() .cloned() - .and_then(|modal| modal.downcast::()) + .and_then(|modal| modal.downcast::()) .is_some() { workspace.dismiss_modal(cx) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 605d9533f2..bc1c471bfa 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -4,7 +4,6 @@ mod modal; pub mod terminal_element; use alacritty_terminal::term::SizeInfo; -use anyhow::Result; use connection::{Event, TerminalConnection}; use dirs::home_dir; use gpui::{ @@ -17,7 +16,6 @@ use project::{LocalWorktree, Project, ProjectPath}; use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::path::{Path, PathBuf}; -use util::ResultExt; use workspace::{Item, Workspace}; use crate::terminal_element::TerminalEl; @@ -50,21 +48,21 @@ actions!( ///Initialize and register all of our action handlers pub fn init(cx: &mut MutableAppContext) { //Global binding overrrides - cx.add_action(Terminal::ctrl_c); - cx.add_action(Terminal::up); - cx.add_action(Terminal::down); - cx.add_action(Terminal::escape); - cx.add_action(Terminal::enter); + 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 actions - cx.add_action(Terminal::deploy); + cx.add_action(TerminalView::deploy); cx.add_action(deploy_modal); - cx.add_action(Terminal::copy); - cx.add_action(Terminal::paste); - cx.add_action(Terminal::clear); + cx.add_action(TerminalView::copy); + cx.add_action(TerminalView::paste); + cx.add_action(TerminalView::clear); } ///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct Terminal { +pub struct TerminalView { connection: ModelHandle, has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received @@ -73,18 +71,14 @@ pub struct Terminal { modal: bool, } -impl Entity for Terminal { +impl Entity for TerminalView { type Event = Event; } -impl Terminal { +impl TerminalView { ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` - fn new( - working_directory: Option, - modal: bool, - cx: &mut ViewContext, - ) -> Result { + fn new(working_directory: Option, modal: bool, cx: &mut ViewContext) -> Self { //The details here don't matter, the terminal will be resized on the first layout let size_info = SizeInfo::new( DEBUG_TERMINAL_WIDTH, @@ -103,18 +97,17 @@ impl Terminal { (shell, envs) }; - let connection = cx.try_add_model(|cx| { - TerminalConnection::new(working_directory, shell, envs, size_info, cx) - })?; + let connection = cx + .add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx)); - Ok(Terminal::from_connection(connection, modal, cx)) + TerminalView::from_connection(connection, modal, cx) } fn from_connection( connection: ModelHandle, modal: bool, cx: &mut ViewContext, - ) -> Terminal { + ) -> TerminalView { cx.observe(&connection, |_, _, cx| cx.notify()).detach(); cx.subscribe(&connection, |this, _, event, cx| match event { Event::Wakeup => { @@ -137,7 +130,7 @@ impl Terminal { }) .detach(); - Terminal { + TerminalView { connection, has_new_content: true, has_bell: false, @@ -152,73 +145,79 @@ impl Terminal { fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { self.connection - .update(cx, |connection, _| connection.clear()); + .read(cx) + .get_terminal() + .map(|term| term.clear()); } ///Create a new Terminal in the current working directory or the user's home directory fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let wd = get_wd_for_workspace(workspace, cx); - if let Some(view) = cx.add_option_view(|cx| Terminal::new(wd, false, cx).log_err()) { - workspace.add_item(Box::new(view), cx); - } + let view = cx.add_view(|cx| TerminalView::new(wd, false, cx)); + workspace.add_item(Box::new(view), cx); } ///Attempt to paste the clipboard into the terminal fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - let term = self.connection.read(cx).term.lock(); - let copy_text = term.selection_to_string(); - match copy_text { - Some(s) => cx.write_to_clipboard(ClipboardItem::new(s)), - None => (), - } + self.connection + .read(cx) + .get_terminal() + .and_then(|term| term.copy()) + .map(|text| cx.write_to_clipboard(ClipboardItem::new(text))); } ///Attempt to paste the clipboard into the terminal fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - if let Some(item) = cx.read_from_clipboard() { - self.connection.update(cx, |connection, _| { - connection.paste(item.text()); - }) - } + cx.read_from_clipboard().map(|item| { + self.connection + .read(cx) + .get_terminal() + .map(|term| term.paste(item.text())); + }); } ///Synthesize the keyboard event corresponding to 'up' fn up(&mut self, _: &Up, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("up").unwrap()); - }); + self.connection + .read(cx) + .get_terminal() + .map(|term| term.try_keystroke(&Keystroke::parse("up").unwrap())); } ///Synthesize the keyboard event corresponding to 'down' fn down(&mut self, _: &Down, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("down").unwrap()); - }); + self.connection + .read(cx) + .get_terminal() + .map(|term| term.try_keystroke(&Keystroke::parse("down").unwrap())); } ///Synthesize the keyboard event corresponding to 'ctrl-c' fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); - }); + self.connection + .read(cx) + .get_terminal() + .map(|term| term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap())); } ///Synthesize the keyboard event corresponding to 'escape' fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("escape").unwrap()); - }); + self.connection + .read(cx) + .get_terminal() + .map(|term| term.try_keystroke(&Keystroke::parse("escape").unwrap())); } ///Synthesize the keyboard event corresponding to 'enter' fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { - self.connection.update(cx, |connection, _| { - connection.try_keystroke(&Keystroke::parse("enter").unwrap()); - }); + self.connection + .read(cx) + .get_terminal() + .map(|term| term.try_keystroke(&Keystroke::parse("enter").unwrap())); } } -impl View for Terminal { +impl View for TerminalView { fn ui_name() -> &'static str { "Terminal" } @@ -252,36 +251,42 @@ impl View for Terminal { } } -impl Item for Terminal { +impl Item for TerminalView { fn tab_content( &self, _detail: Option, tab_theme: &theme::Tab, cx: &gpui::AppContext, ) -> ElementBox { - Flex::row() - .with_child( - Label::new( - self.connection.read(cx).title.clone(), - tab_theme.label.clone(), - ) + let mut flex = Flex::row(); + + let title = match self.connection.read(cx) { + TerminalConnection::Connected(conn) => conn.title, + TerminalConnection::Disconnected { .. } => "Terminal".to_string(), //TODO ask nate about htis + }; + + flex.with_child( + Label::new(title, tab_theme.label.clone()) .aligned() .contained() .boxed(), - ) - .boxed() + ) + .boxed() } fn clone_on_split(&self, cx: &mut ViewContext) -> Option { //From what I can tell, there's no way to tell the current working //Directory of the terminal from outside the shell. There might be //solutions to this, but they are non-trivial and require more IPC - Terminal::new( - self.connection.read(cx).associated_directory.clone(), - false, - cx, - ) - .ok() + + let wd = self + .connection + .read(cx) + .get_terminal() + .and_then(|term| term.associated_directory) + .clone(); + + Some(TerminalView::new(wd, false, cx)) } fn project_path(&self, _cx: &gpui::AppContext) -> Option { diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 514d5ee1d4..0d34914b76 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -105,17 +105,20 @@ impl TerminalEl { MouseButton::Left, move |MouseButtonEvent { position, .. }, cx| { if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) { - conn_handle.update(cx.app, |conn, _cx| { - let mut term = conn.term.lock(); - let (point, side) = mouse_to_cell_data( - position, - origin, - conn.cur_size, - term.renderable_content().display_offset, - ); - term.selection = - Some(Selection::new(SelectionType::Simple, point, side)) - }); + conn_handle.update(cx.app, |connection, cx| { + connection.get_terminal().map(|terminal| { + let (point, side) = mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); + + terminal.mouse_down(point, side); + + cx.notify(); + }); + }) } }, ) @@ -130,13 +133,11 @@ impl TerminalEl { cx.focus_parent_view(); if let Some(conn_handle) = click_connection.upgrade(cx.app) { conn_handle.update(cx.app, |conn, cx| { - let mut term = conn.term.lock(); - let (point, side) = mouse_to_cell_data( position, origin, - conn.cur_size, - term.renderable_content().display_offset, + // conn.cur_size, + // term.renderable_content().display_offset, ); let selection_type = match click_count { @@ -150,9 +151,7 @@ impl TerminalEl { let selection = selection_type.map(|selection_type| { Selection::new(selection_type, point, side) }); - - term.selection = selection; - + conn.set_selection(selection); cx.notify(); }); } @@ -162,22 +161,19 @@ impl TerminalEl { MouseButton::Left, move |_, MouseMovedEvent { position, .. }, cx| { if let Some(conn_handle) = drag_connection.upgrade(cx.app) { - conn_handle.update(cx.app, |conn, cx| { - let mut term = conn.term.lock(); + conn_handle.update(cx.app, |connection, cx| { + connection.get_terminal().map(|terminal| { + let (point, side) = mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); - let (point, side) = mouse_to_cell_data( - position, - origin, - conn.cur_size, - term.renderable_content().display_offset, - ); + terminal.drag(point, side); - if let Some(mut selection) = term.selection.take() { - selection.update(point, side); - term.selection = Some(selection); - } - - cx.notify() + cx.notify() + }); }); } }, @@ -197,14 +193,17 @@ impl Element for TerminalEl { ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { let tcx = TerminalLayoutContext::new(cx.global::(), &cx.font_cache()); - let term = { - let connection = self.connection.upgrade(cx).unwrap().read(cx); - //This locks the terminal, so resize it first. - connection.set_size(make_new_size(constraint, &tcx.cell_width, &tcx.line_height)); - connection.term.lock() - }; + let terminal = self + .connection + .upgrade(cx) + .unwrap() + .read(cx) + .get_terminal() //TODO! + .unwrap(); + //This locks the terminal, so resize it first. + terminal.set_size(make_new_size(constraint, &tcx.cell_width, &tcx.line_height)); - let content = term.renderable_content(); + let (content, grid) = terminal.renderable_content(); //Layout grid cells @@ -219,7 +218,7 @@ impl Element for TerminalEl { //Layout cursor let cursor = layout_cursor( - term.grid(), + grid, cx.text_layout_cache, &tcx, content.cursor.point, @@ -390,14 +389,12 @@ impl Element for TerminalEl { let vertical_scroll = (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER; - if let Some(connection) = self.connection.upgrade(cx.app) { - connection.update(cx.app, |connection, _| { - connection - .term - .lock() - .scroll_display(Scroll::Delta(vertical_scroll.round() as i32)); - }) - } + self.connection + .upgrade(cx.app) + .and_then(|handle| handle.read(cx.app).get_terminal()) + .map(|terminal| { + terminal.scroll(Scroll::Delta(vertical_scroll.round() as i32)); + }); }) .is_some(), Event::KeyDown(KeyDownEvent { keystroke, .. }) => { @@ -412,16 +409,18 @@ impl Element for TerminalEl { self.connection .upgrade(cx.app) - .map(|connection| { - connection - .update(cx.app, |connection, _| connection.try_keystroke(keystroke)) - }) + .and_then(|model_handle| model_handle.read(cx.app).get_terminal()) + .map(|term| term.try_keystroke(keystroke)) .unwrap_or(false) } _ => false, } } + fn metadata(&self) -> Option<&dyn std::any::Any> { + None + } + fn debug( &self, _bounds: gpui::geometry::rect::RectF, diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index 64d602ffb5..b20fe9d685 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -29,7 +29,7 @@ impl<'a> TerminalTestContext<'a> { ); let connection = - cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx).unwrap()); + cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx)); TerminalTestContext { cx, connection } } @@ -40,8 +40,11 @@ impl<'a> TerminalTestContext<'a> { { let command = command.to_string(); self.connection.update(self.cx, |connection, _| { - connection.write_to_pty(command); - connection.write_to_pty("\r".to_string()); + connection.get_terminal().unwrap().write_to_pty(command); + connection + .get_terminal() + .unwrap() + .write_to_pty("\r".to_string()); }); self.connection @@ -58,9 +61,8 @@ impl<'a> TerminalTestContext<'a> { } fn grid_as_str(connection: &TerminalConnection) -> String { - let term = connection.term.lock(); - let grid_iterator = term.renderable_content().display_iter; - let lines = grid_iterator.group_by(|i| i.point.line.0); + let (grid_iterator, _) = connection.get_terminal().unwrap().renderable_content(); + let lines = grid_iterator.display_iter.group_by(|i| i.point.line.0); lines .into_iter() .map(|(_, line)| line.map(|i| i.c).collect::()) From 4f7b6b8b22082f9427fb7d7db4d8d088239c0640 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 18 Jul 2022 16:47:42 -0700 Subject: [PATCH 08/29] Finally finished merging this massive ball of changes --- crates/terminal/src/connection.rs | 75 +++- crates/terminal/src/terminal.rs | 21 +- crates/terminal/src/terminal_element.rs | 350 ++++++++---------- .../src/tests/terminal_test_context.rs | 4 +- 4 files changed, 227 insertions(+), 223 deletions(-) diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index 5809f11eaa..0da1615e9f 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -6,16 +6,16 @@ use alacritty_terminal::{ event::{Event as AlacTermEvent, EventListener, Notify}, event_loop::{EventLoop, Msg, Notifier}, grid::Scroll, - selection::Selection, + index::{Direction, Point}, + selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, - term::{cell::Cell, RenderableContent, SizeInfo, TermMode}, + term::{cell::Cell, RenderableCursor, SizeInfo, TermMode}, tty::{self, setup_env}, Grid, Term, }; -use anyhow::Result; -use futures::channel::mpsc::{ - unbounded::{self, UndboundedSender}, - UnboundedSender, +use futures::{ + channel::mpsc::{unbounded, UnboundedSender}, + StreamExt, }; use settings::{Settings, Shell}; use std::{collections::HashMap, path::PathBuf, sync::Arc}; @@ -67,14 +67,14 @@ impl TerminalConnection { cx: &mut ModelContext, ) -> TerminalConnection { let pty_config = { - let shell = shell.and_then(|shell| match shell { + let alac_shell = shell.clone().and_then(|shell| match shell { Shell::System => None, Shell::Program(program) => Some(Program::Just(program)), Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), }); PtyConfig { - shell, + shell: alac_shell, working_directory: working_directory.clone(), hold: false, } @@ -112,11 +112,10 @@ impl TerminalConnection { } }; - let shell = { + let shell_txt = { let mut buf = [0; 1024]; let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap(); pw.shell.to_string() - // alacritty_unix::default_shell(&pw) }; //And connect them together @@ -135,7 +134,7 @@ impl TerminalConnection { let terminal = Terminal { pty_tx: Notifier(pty_tx), term, - title: DEFAULT_TITLE.to_string(), + title: shell_txt.to_string(), associated_directory: working_directory, }; @@ -145,7 +144,15 @@ impl TerminalConnection { match this.upgrade(&cx) { Some(this) => { this.update(&mut cx, |this, cx| { - terminal.process_terminal_event(event, cx); + match this { + TerminalConnection::Connected(conn) => { + conn.process_terminal_event(event, cx) + } + //There should never be a state where the terminal is disconnected + //And receiving events from the pty + TerminalConnection::Disconnected { .. } => unreachable!(), + } + cx.notify(); }); } @@ -280,16 +287,26 @@ impl Terminal { pub fn take_selection(&self) -> Option { self.term.lock().selection.take() } - ///Sets the selection object on the terminal pub fn set_selection(&self, sel: Option) { self.term.lock().selection = sel; } - ///Get the relevant rendering values from the terminal - pub fn renderable_content(&self) -> (RenderableContent, &Grid) { + pub fn grid(&self) -> Grid { let term = self.term.lock(); - (term.renderable_content(), term.grid()) + term.grid().clone() //TODO: BAD!!!!!!!! + } + + pub fn get_display_offset(&self) -> usize { + self.term.lock().renderable_content().display_offset + } + + pub fn get_selection(&self) -> Option { + self.term.lock().renderable_content().selection //TODO: BAD!!!!! + } + + pub fn get_cursor(&self) -> RenderableCursor { + self.term.lock().renderable_content().cursor } ///Scroll the terminal @@ -297,11 +314,31 @@ impl Terminal { self.term.lock().scroll_display(scroll) } - // pub fn click(&mut self, pos: Vector2F, clicks: usize) {} + pub fn click(&self, point: Point, side: Direction, clicks: usize) { + let selection_type = match clicks { + 0 => return, //This is a release + 1 => Some(SelectionType::Simple), + 2 => Some(SelectionType::Semantic), + 3 => Some(SelectionType::Lines), + _ => None, + }; - // pub fn drag(prev_pos: Vector2F, pos: Vector2F) {} + let selection = + selection_type.map(|selection_type| Selection::new(selection_type, point, side)); - // pub fn mouse_down(pos: Vector2F) {} + self.set_selection(selection); + } + + pub fn drag(&self, point: Point, side: Direction) { + if let Some(mut selection) = self.take_selection() { + selection.update(point, side); + self.set_selection(Some(selection)); + } + } + + pub fn mouse_down(&self, point: Point, side: Direction) { + self.set_selection(Some(Selection::new(SelectionType::Simple, point, side))); + } } impl Drop for TerminalConnection { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index bc1c471bfa..2805e5799c 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -258,20 +258,19 @@ impl Item for TerminalView { tab_theme: &theme::Tab, cx: &gpui::AppContext, ) -> ElementBox { - let mut flex = Flex::row(); - let title = match self.connection.read(cx) { - TerminalConnection::Connected(conn) => conn.title, + TerminalConnection::Connected(conn) => conn.title.clone(), TerminalConnection::Disconnected { .. } => "Terminal".to_string(), //TODO ask nate about htis }; - flex.with_child( - Label::new(title, tab_theme.label.clone()) - .aligned() - .contained() - .boxed(), - ) - .boxed() + Flex::row() + .with_child( + Label::new(title, tab_theme.label.clone()) + .aligned() + .contained() + .boxed(), + ) + .boxed() } fn clone_on_split(&self, cx: &mut ViewContext) -> Option { @@ -283,7 +282,7 @@ impl Item for TerminalView { .connection .read(cx) .get_terminal() - .and_then(|term| term.associated_directory) + .and_then(|term| term.associated_directory.clone()) .clone(); Some(TerminalView::new(wd, false, cx)) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 0d34914b76..d6e8a61685 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -4,7 +4,7 @@ use alacritty_terminal::{ ansi::{Color::Named, NamedColor}, grid::{Dimensions, GridIterator, Indexed, Scroll}, index::{Column as GridCol, Line as GridLine, Point, Side}, - selection::{Selection, SelectionRange, SelectionType}, + selection::SelectionRange, term::{ cell::{Cell, Flags}, SizeInfo, @@ -35,7 +35,7 @@ use util::ResultExt; use std::{cmp::min, ops::Range}; use std::{fmt::Debug, ops::Sub}; -use crate::{color_translation::convert_color, connection::TerminalConnection, Terminal}; +use crate::{color_translation::convert_color, connection::TerminalConnection, TerminalView}; use self::terminal_layout_context::TerminalLayoutContext; @@ -48,7 +48,7 @@ const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalEl { connection: WeakModelHandle, - view: WeakViewHandle, + view: WeakViewHandle, modal: bool, } @@ -59,9 +59,100 @@ pub struct LineHeight(f32); ///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than struct PaneRelativePos(Vector2F); +#[derive(Clone, Debug, Default)] +struct LayoutCell { + point: Point, + text: Line, +} + +impl LayoutCell { + fn new(point: Point, text: Line) -> LayoutCell { + LayoutCell { point, text } + } + + fn paint( + &self, + origin: Vector2F, + layout: &LayoutState, + visible_bounds: RectF, + cx: &mut PaintContext, + ) { + let pos = point_to_absolute(origin, self.point, layout); + self.text + .paint(pos, visible_bounds, layout.line_height.0, cx); + } +} + +#[derive(Clone, Debug, Default)] +struct LayoutRect { + point: Point, + num_of_cells: usize, + color: Color, +} + +impl LayoutRect { + fn new(point: Point, num_of_cells: usize, color: Color) -> LayoutRect { + LayoutRect { + point, + num_of_cells, + color, + } + } + + fn extend(&self) -> Self { + LayoutRect { + point: self.point, + num_of_cells: self.num_of_cells + 1, + color: self.color, + } + } + + fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) { + let position = point_to_absolute(origin, self.point, layout); + + let size = vec2f(layout.em_width.0.ceil(), layout.line_height.0); + + cx.scene.push_quad(Quad { + bounds: RectF::new(position, size), + background: Some(self.color), + border: Default::default(), + corner_radius: 0., + }) + } +} + +fn point_to_absolute(origin: Vector2F, point: Point, layout: &LayoutState) -> Vector2F { + vec2f( + (origin.x() + point.column as f32 * layout.em_width.0).floor(), + origin.y() + point.line as f32 * layout.line_height.0, + ) +} + +struct RelativeHighlightedRange { + line_index: usize, + range: Range, +} + +impl RelativeHighlightedRange { + fn new(line_index: usize, range: Range) -> Self { + RelativeHighlightedRange { line_index, range } + } + + fn to_highlighted_range_line( + &self, + origin: Vector2F, + layout: &LayoutState, + ) -> HighlightedRangeLine { + let start_x = origin.x() + self.range.start as f32 * layout.em_width.0; + let end_x = origin.x() + self.range.end as f32 * layout.em_width.0 + layout.em_width.0; + + return HighlightedRangeLine { start_x, end_x }; + } +} + ///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos { - PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating + PaneRelativePos(mouse_position.sub(origin)) } ///The information generated during layout that is nescessary for painting @@ -74,11 +165,12 @@ pub struct LayoutState { cursor: Option, background_color: Color, selection_color: Color, + cur_size: SizeInfo, } impl TerminalEl { pub fn new( - view: WeakViewHandle, + view: WeakViewHandle, connection: WeakModelHandle, modal: bool, ) -> TerminalEl { @@ -94,6 +186,7 @@ impl TerminalEl { origin: Vector2F, view_id: usize, visible_bounds: RectF, + cur_size: SizeInfo, cx: &mut PaintContext, ) { let mouse_down_connection = self.connection.clone(); @@ -132,27 +225,19 @@ impl TerminalEl { cx| { cx.focus_parent_view(); if let Some(conn_handle) = click_connection.upgrade(cx.app) { - conn_handle.update(cx.app, |conn, cx| { - let (point, side) = mouse_to_cell_data( - position, - origin, - // conn.cur_size, - // term.renderable_content().display_offset, - ); + conn_handle.update(cx.app, |connection, cx| { + connection.get_terminal().map(|terminal| { + let (point, side) = mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); - let selection_type = match click_count { - 0 => return, //This is a release - 1 => Some(SelectionType::Simple), - 2 => Some(SelectionType::Semantic), - 3 => Some(SelectionType::Lines), - _ => None, - }; + terminal.click(point, side, click_count); - let selection = selection_type.map(|selection_type| { - Selection::new(selection_type, point, side) - }); - conn.set_selection(selection); - cx.notify(); + cx.notify(); + }) }); } }, @@ -201,28 +286,31 @@ impl Element for TerminalEl { .get_terminal() //TODO! .unwrap(); //This locks the terminal, so resize it first. - terminal.set_size(make_new_size(constraint, &tcx.cell_width, &tcx.line_height)); - - let (content, grid) = terminal.renderable_content(); + let cur_size = make_new_size(constraint, &tcx.cell_width, &tcx.line_height); + terminal.set_size(cur_size); + let grid = terminal.grid(); + let selection = terminal.get_selection(); + let display_offset = terminal.get_display_offset(); + let cursor = terminal.get_cursor(); //Layout grid cells let (cells, rects, highlights) = layout_grid( - content.display_iter, + grid.display_iter(), &tcx.text_style, tcx.terminal_theme, cx.text_layout_cache, self.modal, - content.selection, + selection, ); //Layout cursor let cursor = layout_cursor( - grid, + &grid, cx.text_layout_cache, &tcx, - content.cursor.point, - content.display_offset, + cursor.point, + display_offset, constraint, ); @@ -237,12 +325,13 @@ impl Element for TerminalEl { ( constraint.max, LayoutState { + cells, line_height: tcx.line_height, em_width: tcx.cell_width, cursor, background_color, selection_color: tcx.selection_color, - cells, + cur_size, rects, highlights, }, @@ -270,7 +359,7 @@ impl Element for TerminalEl { let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse - self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, cx); + self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.cur_size, cx); cx.paint_layer(clip_bounds, |cx| { //Start with a background color @@ -281,58 +370,30 @@ impl Element for TerminalEl { corner_radius: 0., }); - //Draw cell backgrounds - for layout_line in &layout.layout_cells { - for layout_cell in &layout_line.cells { - let position = vec2f( - (origin.x() + layout_cell.point.column as f32 * layout.em_width.0) - .floor(), - origin.y() + layout_cell.point.line as f32 * layout.line_height.0, - ); - let size = vec2f(layout.em_width.0.ceil(), layout.line_height.0); - - cx.scene.push_quad(Quad { - bounds: RectF::new(position, size), - background: Some(layout_cell.background_color), - border: Default::default(), - corner_radius: 0., - }) - } + for rect in &layout.rects { + rect.paint(origin, &layout, cx) } }); //Draw Selection cx.paint_layer(clip_bounds, |cx| { - let mut highlight_y = None; - let highlight_lines = layout - .layout_cells - .iter() - .filter_map(|line| { - if let Some(range) = &line.highlighted_range { - if let None = highlight_y { - highlight_y = Some( - origin.y() - + line.cells[0].point.line as f32 * layout.line_height.0, - ); - } - let start_x = origin.x() - + line.cells[range.start].point.column as f32 * layout.em_width.0; - let end_x = origin.x() - + line.cells[range.end].point.column as f32 * layout.em_width.0 - + layout.em_width.0; + let start_y = layout.highlights.get(0).map(|highlight| { + origin.y() + highlight.line_index as f32 * layout.line_height.0 + }); - return Some(HighlightedRangeLine { start_x, end_x }); - } else { - return None; - } - }) - .collect::>(); + if let Some(y) = start_y { + let range_lines = layout + .highlights + .iter() + .map(|relative_highlight| { + relative_highlight.to_highlighted_range_line(origin, layout) + }) + .collect::>(); - if let Some(y) = highlight_y { let hr = HighlightedRange { start_y: y, //Need to change this line_height: layout.line_height.0, - lines: highlight_lines, + lines: range_lines, color: layout.selection_color, //Copied from editor. TODO: move to theme or something corner_radius: 0.15 * layout.line_height.0, @@ -342,23 +403,8 @@ impl Element for TerminalEl { }); cx.paint_layer(clip_bounds, |cx| { - for layout_line in &layout.layout_cells { - for layout_cell in &layout_line.cells { - let point = layout_cell.point; - - //Don't actually know the start_x for a line, until here: - let cell_origin = vec2f( - (origin.x() + point.column as f32 * layout.em_width.0).floor(), - origin.y() + point.line as f32 * layout.line_height.0, - ); - - layout_cell.text.paint( - cell_origin, - visible_bounds, - layout.line_height.0, - cx, - ); - } + for cell in &layout.cells { + cell.paint(origin, layout, visible_bounds, cx); } }); @@ -521,50 +567,6 @@ fn make_new_size( ) } -#[derive(Clone, Debug, Default)] -struct LayoutCell { - point: Point, - text: Line, -} - -impl LayoutCell { - fn new(point: Point, text: Line) -> LayoutCell { - LayoutCell { point, text } - } -} - -#[derive(Clone, Debug, Default)] -struct LayoutRect { - pos: Point, - num_of_cells: usize, - color: Color, -} - -impl LayoutRect { - fn new(pos: Point, num_of_cells: usize, color: Color) -> LayoutRect { - LayoutRect { - pos, - num_of_cells, - color, - } - } - - fn extend(&mut self) { - self.num_of_cells += 1; - } -} - -struct RelativeHighlightedRange { - line_index: usize, - range: Range, -} - -impl RelativeHighlightedRange { - fn new(line_index: usize, range: Range) -> Self { - RelativeHighlightedRange { line_index, range } - } -} - fn layout_grid( grid: GridIterator, text_style: &TextStyle, @@ -602,39 +604,39 @@ fn layout_grid( //Expand background rect range { - match (cell.bg, cur_alac_color) { - (Named(NamedColor::Background), Some(_)) => { - //Skip color, end background - cur_alac_color = None; - rects.push(cur_rect.take().unwrap()); + if matches!(cell.bg, Named(NamedColor::Background)) { + //Continue to next cell, resetting variables if nescessary + cur_alac_color = None; + if let Some(rect) = cur_rect { + rects.push(rect); + cur_rect = None } - (bg, Some(cur_color)) => { - //If they're the same, extend the match - if bg == cur_color { - cur_rect.unwrap().extend() - } else { - //If they differ, end background and restart - cur_alac_color = None; - rects.push(cur_rect.take().unwrap()); + } - cur_alac_color = Some(bg); + match cur_alac_color { + Some(cur_color) => { + if cell.bg == cur_color { + cur_rect = cur_rect.take().map(|rect| rect.extend()); + } else { + cur_alac_color = Some(cell.bg); + if let Some(_) = cur_rect { + rects.push(cur_rect.take().unwrap()); + } cur_rect = Some(LayoutRect::new( Point::new(line_index as i32, cell.point.column.0 as i32), 1, - convert_color(&bg, &terminal_theme.colors, modal), + convert_color(&cell.bg, &terminal_theme.colors, modal), )); } } - (bg, None) if !matches!(bg, Named(NamedColor::Background)) => { - //install new background - cur_alac_color = Some(bg); + None => { + cur_alac_color = Some(cell.bg); cur_rect = Some(LayoutRect::new( Point::new(line_index as i32, cell.point.column.0 as i32), 1, - convert_color(&bg, &terminal_theme.colors, modal), + convert_color(&cell.bg, &terminal_theme.colors, modal), )); } - (_, _) => {} //Only happens when bg is NamedColor::Background } } @@ -887,38 +889,4 @@ mod test { ) ); } - - #[test] - fn test_mouse_to_selection_off_edge() { - let term_width = 100.; - let term_height = 200.; - let cell_width = 10.; - let line_height = 20.; - let mouse_pos_x = 100.; //Window relative - let mouse_pos_y = 100.; //Window relative - let origin_x = 10.; - let origin_y = 20.; - - let cur_size = alacritty_terminal::term::SizeInfo::new( - term_width, - term_height, - cell_width, - line_height, - 0., - 0., - false, - ); - - 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 (point, _) = - crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); - assert_eq!( - point, - alacritty_terminal::index::Point::new( - alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32), - alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize), - ) - ); - } } diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index b20fe9d685..f18d9ca153 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -61,8 +61,8 @@ impl<'a> TerminalTestContext<'a> { } fn grid_as_str(connection: &TerminalConnection) -> String { - let (grid_iterator, _) = connection.get_terminal().unwrap().renderable_content(); - let lines = grid_iterator.display_iter.group_by(|i| i.point.line.0); + let grid = connection.get_terminal().unwrap().grid(); + let lines = grid.display_iter().group_by(|i| i.point.line.0); lines .into_iter() .map(|(_, line)| line.map(|i| i.c).collect::()) From 005e2cb2bec9754b446dcbe4b360b7e7b5a2f368 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 18 Jul 2022 17:05:24 -0700 Subject: [PATCH 09/29] Compiling again... finally --- crates/terminal/src/terminal_element.rs | 51 ++++++++++--------- .../terminal_layout_context.rs | 6 +-- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index d6e8a61685..0c65fe0e3f 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -37,7 +37,7 @@ use std::{fmt::Debug, ops::Sub}; use crate::{color_translation::convert_color, connection::TerminalConnection, TerminalView}; -use self::terminal_layout_context::TerminalLayoutContext; +use self::terminal_layout_context::TerminalLayoutTheme; ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I @@ -110,7 +110,10 @@ impl LayoutRect { fn paint(&self, origin: Vector2F, layout: &LayoutState, cx: &mut PaintContext) { let position = point_to_absolute(origin, self.point, layout); - let size = vec2f(layout.em_width.0.ceil(), layout.line_height.0); + let size = vec2f( + (layout.em_width.0.ceil() * self.num_of_cells as f32).ceil(), + layout.line_height.0, + ); cx.scene.push_quad(Quad { bounds: RectF::new(position, size), @@ -128,6 +131,7 @@ fn point_to_absolute(origin: Vector2F, point: Point, layout: &LayoutSt ) } +#[derive(Clone, Debug, Default)] struct RelativeHighlightedRange { line_index: usize, range: Range, @@ -276,7 +280,7 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let tcx = TerminalLayoutContext::new(cx.global::(), &cx.font_cache()); + let tcx = TerminalLayoutTheme::new(cx.global::(), &cx.font_cache()); let terminal = self .connection @@ -483,7 +487,7 @@ impl Element for TerminalEl { fn layout_cursor( grid: &Grid, text_layout_cache: &TextLayoutCache, - tcx: &TerminalLayoutContext, + tcx: &TerminalLayoutTheme, cursor_point: Point, display_offset: usize, constraint: SizeConstraint, @@ -519,7 +523,7 @@ fn layout_cursor( fn layout_cursor_text( grid: &Grid, text_layout_cache: &TextLayoutCache, - tcx: &TerminalLayoutContext, + tcx: &TerminalLayoutTheme, ) -> Line { let cursor_point = grid.cursor.point; let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string(); @@ -611,17 +615,25 @@ fn layout_grid( rects.push(rect); cur_rect = None } - } - - match cur_alac_color { - Some(cur_color) => { - if cell.bg == cur_color { - cur_rect = cur_rect.take().map(|rect| rect.extend()); - } else { - cur_alac_color = Some(cell.bg); - if let Some(_) = cur_rect { - rects.push(cur_rect.take().unwrap()); + } else { + match cur_alac_color { + Some(cur_color) => { + if cell.bg == cur_color { + cur_rect = cur_rect.take().map(|rect| rect.extend()); + } else { + cur_alac_color = Some(cell.bg); + if let Some(_) = cur_rect { + rects.push(cur_rect.take().unwrap()); + } + cur_rect = Some(LayoutRect::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + 1, + convert_color(&cell.bg, &terminal_theme.colors, modal), + )); } + } + None => { + cur_alac_color = Some(cell.bg); cur_rect = Some(LayoutRect::new( Point::new(line_index as i32, cell.point.column.0 as i32), 1, @@ -629,14 +641,6 @@ fn layout_grid( )); } } - None => { - cur_alac_color = Some(cell.bg); - cur_rect = Some(LayoutRect::new( - Point::new(line_index as i32, cell.point.column.0 as i32), - 1, - convert_color(&cell.bg, &terminal_theme.colors, modal), - )); - } } } @@ -671,6 +675,7 @@ fn layout_grid( rects.push(cur_rect.take().unwrap()); } } + (cells, rects, highlight_ranges) } diff --git a/crates/terminal/src/terminal_element/terminal_layout_context.rs b/crates/terminal/src/terminal_element/terminal_layout_context.rs index 5ddbbdec7b..edfe71c659 100644 --- a/crates/terminal/src/terminal_element/terminal_layout_context.rs +++ b/crates/terminal/src/terminal_element/terminal_layout_context.rs @@ -1,6 +1,6 @@ use super::*; -pub struct TerminalLayoutContext<'a> { +pub struct TerminalLayoutTheme<'a> { pub line_height: LineHeight, pub cell_width: CellWidth, pub text_style: TextStyle, @@ -8,7 +8,7 @@ pub struct TerminalLayoutContext<'a> { pub terminal_theme: &'a TerminalStyle, } -impl<'a> TerminalLayoutContext<'a> { +impl<'a> TerminalLayoutTheme<'a> { pub fn new(settings: &'a Settings, font_cache: &FontCache) -> Self { let text_style = Self::make_text_style(font_cache, &settings); let line_height = LineHeight(font_cache.line_height(text_style.font_size)); @@ -16,7 +16,7 @@ impl<'a> TerminalLayoutContext<'a> { let selection_color = settings.theme.editor.selection.selection; let terminal_theme = &settings.theme.terminal; - TerminalLayoutContext { + TerminalLayoutTheme { line_height, cell_width, text_style, From ef1a32ee9242f076d434c3b661475b046fe37336 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 19 Jul 2022 10:30:46 -0700 Subject: [PATCH 10/29] Done updating rendering performance for now. Further changes would require more fundamental work, I'm still not really happy with it as is though. Will probably take a few hours to clean the code though. --- crates/terminal/src/connection.rs | 52 +++++++++------ crates/terminal/src/terminal.rs | 10 +-- crates/terminal/src/terminal_element.rs | 64 +++++++++---------- .../src/tests/terminal_test_context.rs | 18 ++++-- 4 files changed, 82 insertions(+), 62 deletions(-) diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index 0da1615e9f..aaa8d639bc 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -7,11 +7,11 @@ use alacritty_terminal::{ event_loop::{EventLoop, Msg, Notifier}, grid::Scroll, index::{Direction, Point}, - selection::{Selection, SelectionRange, SelectionType}, + selection::{Selection, SelectionType}, sync::FairMutex, - term::{cell::Cell, RenderableCursor, SizeInfo, TermMode}, + term::{RenderableContent, SizeInfo, TermMode}, tty::{self, setup_env}, - Grid, Term, + Term, }; use futures::{ channel::mpsc::{unbounded, UnboundedSender}, @@ -54,7 +54,7 @@ pub enum TerminalConnection { Disconnected { directory: Option, shell: Option, - error: std::io::Error, + error: Option, }, } @@ -107,15 +107,23 @@ impl TerminalConnection { return TerminalConnection::Disconnected { directory: working_directory, shell, - error, + error: Some(error), }; } }; let shell_txt = { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap(); - pw.shell.to_string() + match shell { + Some(Shell::System) | None => { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap(); + pw.shell.to_string() + } + Some(Shell::Program(program)) => program, + Some(Shell::WithArguments { program, args }) => { + format!("{} {}", program, args.join(" ")) + } + } }; //And connect them together @@ -292,23 +300,29 @@ impl Terminal { self.term.lock().selection = sel; } - pub fn grid(&self) -> Grid { - let term = self.term.lock(); - term.grid().clone() //TODO: BAD!!!!!!!! + pub fn render_lock(&self, new_size: Option, f: F) -> T + where + F: FnOnce(RenderableContent) -> T, + { + if let Some(new_size) = new_size { + self.pty_tx.0.send(Msg::Resize(new_size)).ok(); //Give the PTY a chance to react to the new size + //TODO: Is this bad for performance? + } + + let mut term = self.term.lock(); //Lock + + if let Some(new_size) = new_size { + term.resize(new_size); //Reflow + } + + let content = term.renderable_content(); + f(content) } pub fn get_display_offset(&self) -> usize { self.term.lock().renderable_content().display_offset } - pub fn get_selection(&self) -> Option { - self.term.lock().renderable_content().selection //TODO: BAD!!!!! - } - - pub fn get_cursor(&self) -> RenderableCursor { - self.term.lock().renderable_content().cursor - } - ///Scroll the terminal pub fn scroll(&self, scroll: Scroll) { self.term.lock().scroll_display(scroll) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 2805e5799c..408706042e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -364,10 +364,12 @@ fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option current_project_directory(workspace, cx), WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), WorkingDirectory::AlwaysHome => None, - WorkingDirectory::Always { directory } => shellexpand::full(&directory) - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf()) - .filter(|dir| dir.is_dir()), + WorkingDirectory::Always { directory } => { + shellexpand::full(&directory) //TODO handle this better + .ok() + .map(|dir| Path::new(&dir.to_string()).to_path_buf()) + .filter(|dir| dir.is_dir()) + } }; res.or_else(|| home_dir()) } diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 0c65fe0e3f..f8f118ec67 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -9,7 +9,6 @@ use alacritty_terminal::{ cell::{Cell, Flags}, SizeInfo, }, - Grid, }; use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ @@ -282,41 +281,40 @@ impl Element for TerminalEl { ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { let tcx = TerminalLayoutTheme::new(cx.global::(), &cx.font_cache()); + //This locks the terminal, so resize it first. + //Layout grid cells + let cur_size = make_new_size(constraint, &tcx.cell_width, &tcx.line_height); + let terminal = self .connection .upgrade(cx) .unwrap() .read(cx) - .get_terminal() //TODO! + .get_terminal() .unwrap(); - //This locks the terminal, so resize it first. - let cur_size = make_new_size(constraint, &tcx.cell_width, &tcx.line_height); - terminal.set_size(cur_size); - let grid = terminal.grid(); - let selection = terminal.get_selection(); - let display_offset = terminal.get_display_offset(); - let cursor = terminal.get_cursor(); - //Layout grid cells + let (cursor, cells, rects, highlights) = terminal.render_lock(Some(cur_size), |content| { + let (cells, rects, highlights) = layout_grid( + content.display_iter, + &tcx.text_style, + tcx.terminal_theme, + cx.text_layout_cache, + self.modal, + content.selection, + ); - let (cells, rects, highlights) = layout_grid( - grid.display_iter(), - &tcx.text_style, - tcx.terminal_theme, - cx.text_layout_cache, - self.modal, - selection, - ); + //Layout cursor + let cursor = layout_cursor( + // grid, + cx.text_layout_cache, + &tcx, + content.cursor.point, + content.display_offset, + constraint, + ); - //Layout cursor - let cursor = layout_cursor( - &grid, - cx.text_layout_cache, - &tcx, - cursor.point, - display_offset, - constraint, - ); + (cursor, cells, rects, highlights) + }); //Select background color let background_color = if self.modal { @@ -406,6 +404,7 @@ impl Element for TerminalEl { } }); + //Draw the text cells cx.paint_layer(clip_bounds, |cx| { for cell in &layout.cells { cell.paint(origin, layout, visible_bounds, cx); @@ -484,15 +483,16 @@ impl Element for TerminalEl { } } +///TODO: Fix cursor rendering with alacritty fork fn layout_cursor( - grid: &Grid, + // grid: &Grid, text_layout_cache: &TextLayoutCache, tcx: &TerminalLayoutTheme, cursor_point: Point, display_offset: usize, constraint: SizeConstraint, ) -> Option { - let cursor_text = layout_cursor_text(grid, text_layout_cache, tcx); + let cursor_text = layout_cursor_text(/*grid,*/ cursor_point, text_layout_cache, tcx); get_cursor_shape( cursor_point.line.0 as usize, cursor_point.column.0 as usize, @@ -521,12 +521,12 @@ fn layout_cursor( } fn layout_cursor_text( - grid: &Grid, + // grid: &Grid, + _cursor_point: Point, text_layout_cache: &TextLayoutCache, tcx: &TerminalLayoutTheme, ) -> Line { - let cursor_point = grid.cursor.point; - let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string(); + let cursor_text = " "; //grid[cursor_point.line][cursor_point.column].c.to_string(); text_layout_cache.layout_str( &cursor_text, diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index f18d9ca153..1297d5c963 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -61,13 +61,17 @@ impl<'a> TerminalTestContext<'a> { } fn grid_as_str(connection: &TerminalConnection) -> String { - let grid = connection.get_terminal().unwrap().grid(); - let lines = grid.display_iter().group_by(|i| i.point.line.0); - lines - .into_iter() - .map(|(_, line)| line.map(|i| i.c).collect::()) - .collect::>() - .join("\n") + connection + .get_terminal() + .unwrap() + .render_lock(None, |content| { + let lines = content.display_iter.group_by(|i| i.point.line.0); + lines + .into_iter() + .map(|(_, line)| line.map(|i| i.c).collect::()) + .collect::>() + .join("\n") + }) } } From 18079ced201a79215eccd07970ac6d691e9e72f7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 19 Jul 2022 16:45:48 -0700 Subject: [PATCH 11/29] Updated alacritty version --- Cargo.lock | 10 +- crates/terminal/Cargo.toml | 2 +- crates/terminal/src/connection.rs | 28 +- crates/terminal/src/terminal.rs | 17 +- crates/terminal/src/terminal_element.rs | 385 ++++++++---------- .../terminal_layout_context.rs | 20 +- .../src/tests/terminal_test_context.rs | 15 +- 7 files changed, 204 insertions(+), 273 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0068911060..acb0bec980 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,8 +62,7 @@ dependencies = [ [[package]] name = "alacritty_config_derive" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77044c45bdb871e501b5789ad16293ecb619e5733b60f4bb01d1cb31c463c336" +source = "git+https://github.com/zed-industries/alacritty?rev=90647e167c036530176dc5b579222c5886286f63#90647e167c036530176dc5b579222c5886286f63" dependencies = [ "proc-macro2", "quote", @@ -72,14 +71,13 @@ dependencies = [ [[package]] name = "alacritty_terminal" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fb5d4af84e39f9754d039ff6de2233c8996dbae0af74910156e559e5766e2f" +version = "0.17.0-dev" +source = "git+https://github.com/zed-industries/alacritty?rev=90647e167c036530176dc5b579222c5886286f63#90647e167c036530176dc5b579222c5886286f63" dependencies = [ "alacritty_config_derive", "base64 0.13.0", "bitflags", - "dirs 3.0.2", + "dirs 4.0.0", "libc", "log", "mio 0.6.23", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index c81be6c524..18159a251a 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -8,7 +8,7 @@ path = "src/terminal.rs" doctest = false [dependencies] -alacritty_terminal = "0.16.1" +alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "90647e167c036530176dc5b579222c5886286f63"} editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index aaa8d639bc..df09496f5c 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -3,13 +3,13 @@ mod keymappings; use alacritty_terminal::{ ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig}, - event::{Event as AlacTermEvent, EventListener, Notify}, + event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, event_loop::{EventLoop, Msg, Notifier}, grid::Scroll, index::{Direction, Point}, selection::{Selection, SelectionType}, sync::FairMutex, - term::{RenderableContent, SizeInfo, TermMode}, + term::{test::TermSize, RenderableContent, TermMode}, tty::{self, setup_env}, Term, }; @@ -22,7 +22,10 @@ use std::{collections::HashMap, path::PathBuf, sync::Arc}; use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; -use crate::color_translation::{get_color_at_index, to_alac_rgb}; +use crate::{ + color_translation::{get_color_at_index, to_alac_rgb}, + terminal_element::TerminalDimensions, +}; use self::keymappings::to_esc_str; @@ -63,7 +66,7 @@ impl TerminalConnection { working_directory: Option, shell: Option, env: Option>, - initial_size: SizeInfo, + initial_size: TerminalDimensions, cx: &mut ModelContext, ) -> TerminalConnection { let pty_config = { @@ -97,11 +100,11 @@ impl TerminalConnection { let (events_tx, mut events_rx) = unbounded(); //Set up the terminal... - let term = Term::new(&config, initial_size, ZedListener(events_tx.clone())); + let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone())); let term = Arc::new(FairMutex::new(term)); //Setup the pty... - let pty = match tty::new(&pty_config, &initial_size, None) { + let pty = match tty::new(&pty_config, initial_size.into(), None) { Ok(pty) => pty, Err(error) => { return TerminalConnection::Disconnected { @@ -237,6 +240,7 @@ impl Terminal { cx.emit(Event::Bell); } AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), + AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"), } } @@ -252,9 +256,11 @@ impl Terminal { } ///Resize the terminal and the PTY. This locks the terminal. - pub fn set_size(&self, new_size: SizeInfo) { + pub fn set_size(&self, new_size: WindowSize) { self.pty_tx.0.send(Msg::Resize(new_size)).ok(); - self.term.lock().resize(new_size); + + let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize); + self.term.lock().resize(term_size); } pub fn clear(&self) { @@ -300,13 +306,13 @@ impl Terminal { self.term.lock().selection = sel; } - pub fn render_lock(&self, new_size: Option, f: F) -> T + pub fn render_lock(&self, new_size: Option, f: F) -> T where F: FnOnce(RenderableContent) -> T, { if let Some(new_size) = new_size { - self.pty_tx.0.send(Msg::Resize(new_size)).ok(); //Give the PTY a chance to react to the new size - //TODO: Is this bad for performance? + self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size + //TODO: Is this bad for performance? } let mut term = self.term.lock(); //Lock diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 408706042e..fc35d0eeea 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -3,12 +3,11 @@ pub mod connection; mod modal; pub mod terminal_element; -use alacritty_terminal::term::SizeInfo; use connection::{Event, TerminalConnection}; use dirs::home_dir; use gpui::{ - actions, elements::*, keymap::Keystroke, AppContext, ClipboardItem, Entity, ModelHandle, - MutableAppContext, View, ViewContext, + actions, elements::*, geometry::vector::vec2f, keymap::Keystroke, AppContext, ClipboardItem, + Entity, ModelHandle, MutableAppContext, View, ViewContext, }; use modal::deploy_modal; @@ -16,6 +15,8 @@ use project::{LocalWorktree, Project, ProjectPath}; use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::path::{Path, PathBuf}; +use terminal_element::TerminalDimensions; + use workspace::{Item, Workspace}; use crate::terminal_element::TerminalEl; @@ -80,14 +81,10 @@ impl TerminalView { ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` fn new(working_directory: Option, modal: bool, cx: &mut ViewContext) -> Self { //The details here don't matter, the terminal will be resized on the first layout - let size_info = SizeInfo::new( - DEBUG_TERMINAL_WIDTH, - DEBUG_TERMINAL_HEIGHT, - DEBUG_CELL_WIDTH, + let size_info = TerminalDimensions::new( DEBUG_LINE_HEIGHT, - 0., - 0., - false, + DEBUG_CELL_WIDTH, + vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), ); let (shell, envs) = { diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index f8f118ec67..249bf90db7 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -1,14 +1,12 @@ -mod terminal_layout_context; +pub mod terminal_layout_context; use alacritty_terminal::{ ansi::{Color::Named, NamedColor}, + event::WindowSize, grid::{Dimensions, GridIterator, Indexed, Scroll}, index::{Column as GridCol, Line as GridLine, Point, Side}, selection::SelectionRange, - term::{ - cell::{Cell, Flags}, - SizeInfo, - }, + term::cell::{Cell, Flags}, }; use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ @@ -36,7 +34,7 @@ use std::{fmt::Debug, ops::Sub}; use crate::{color_translation::convert_color, connection::TerminalConnection, TerminalView}; -use self::terminal_layout_context::TerminalLayoutTheme; +use self::terminal_layout_context::TerminalLayoutData; ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I @@ -51,12 +49,74 @@ pub struct TerminalEl { modal: bool, } -///New type pattern so I don't mix these two up -pub struct CellWidth(f32); -pub struct LineHeight(f32); +#[derive(Clone, Copy, Debug)] +pub struct TerminalDimensions { + pub cell_width: f32, + pub line_height: f32, + pub height: f32, + pub width: f32, +} -///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than -struct PaneRelativePos(Vector2F); +impl TerminalDimensions { + pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self { + TerminalDimensions { + cell_width, + line_height, + width: size.x(), + height: size.y(), + } + } + + pub fn num_lines(&self) -> usize { + (self.height / self.line_height).floor() as usize + } + + pub fn num_columns(&self) -> usize { + (self.width / self.cell_width).floor() as usize + } + + pub fn height(&self) -> f32 { + self.height + } + + pub fn width(&self) -> f32 { + self.width + } + + pub fn cell_width(&self) -> f32 { + self.cell_width + } + + pub fn line_height(&self) -> f32 { + self.line_height + } +} + +//TODO look at what TermSize is +impl Into for TerminalDimensions { + fn into(self) -> WindowSize { + WindowSize { + num_lines: self.num_lines() as u16, + num_cols: self.num_columns() as u16, + cell_width: self.cell_width() as u16, + cell_height: self.line_height() as u16, + } + } +} + +impl Dimensions for TerminalDimensions { + fn total_lines(&self) -> usize { + self.num_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer... + } + + fn screen_lines(&self) -> usize { + self.num_lines() + } + + fn columns(&self) -> usize { + self.num_columns() + } +} #[derive(Clone, Debug, Default)] struct LayoutCell { @@ -78,7 +138,7 @@ impl LayoutCell { ) { let pos = point_to_absolute(origin, self.point, layout); self.text - .paint(pos, visible_bounds, layout.line_height.0, cx); + .paint(pos, visible_bounds, layout.size.line_height, cx); } } @@ -110,8 +170,8 @@ impl LayoutRect { let position = point_to_absolute(origin, self.point, layout); let size = vec2f( - (layout.em_width.0.ceil() * self.num_of_cells as f32).ceil(), - layout.line_height.0, + (layout.size.cell_width.ceil() * self.num_of_cells as f32).ceil(), + layout.size.line_height, ); cx.scene.push_quad(Quad { @@ -125,8 +185,8 @@ impl LayoutRect { fn point_to_absolute(origin: Vector2F, point: Point, layout: &LayoutState) -> Vector2F { vec2f( - (origin.x() + point.column as f32 * layout.em_width.0).floor(), - origin.y() + point.line as f32 * layout.line_height.0, + (origin.x() + point.column as f32 * layout.size.cell_width).floor(), + origin.y() + point.line as f32 * layout.size.line_height, ) } @@ -146,29 +206,23 @@ impl RelativeHighlightedRange { origin: Vector2F, layout: &LayoutState, ) -> HighlightedRangeLine { - let start_x = origin.x() + self.range.start as f32 * layout.em_width.0; - let end_x = origin.x() + self.range.end as f32 * layout.em_width.0 + layout.em_width.0; + let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width; + let end_x = + origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width; return HighlightedRangeLine { start_x, end_x }; } } -///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position -fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos { - PaneRelativePos(mouse_position.sub(origin)) -} - ///The information generated during layout that is nescessary for painting pub struct LayoutState { cells: Vec, rects: Vec, highlights: Vec, - line_height: LineHeight, - em_width: CellWidth, cursor: Option, background_color: Color, selection_color: Color, - cur_size: SizeInfo, + size: TerminalDimensions, } impl TerminalEl { @@ -189,7 +243,7 @@ impl TerminalEl { origin: Vector2F, view_id: usize, visible_bounds: RectF, - cur_size: SizeInfo, + cur_size: TerminalDimensions, cx: &mut PaintContext, ) { let mouse_down_connection = self.connection.clone(); @@ -279,11 +333,8 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let tcx = TerminalLayoutTheme::new(cx.global::(), &cx.font_cache()); - - //This locks the terminal, so resize it first. - //Layout grid cells - let cur_size = make_new_size(constraint, &tcx.cell_width, &tcx.line_height); + let layout = + TerminalLayoutData::new(cx.global::(), &cx.font_cache(), constraint.max); let terminal = self .connection @@ -293,34 +344,35 @@ impl Element for TerminalEl { .get_terminal() .unwrap(); - let (cursor, cells, rects, highlights) = terminal.render_lock(Some(cur_size), |content| { - let (cells, rects, highlights) = layout_grid( - content.display_iter, - &tcx.text_style, - tcx.terminal_theme, - cx.text_layout_cache, - self.modal, - content.selection, - ); + let (cursor, cells, rects, highlights) = + terminal.render_lock(Some(layout.size.clone()), |content| { + let (cells, rects, highlights) = layout_grid( + content.display_iter, + &layout.text_style, + layout.terminal_theme, + cx.text_layout_cache, + self.modal, + content.selection, + ); - //Layout cursor - let cursor = layout_cursor( - // grid, - cx.text_layout_cache, - &tcx, - content.cursor.point, - content.display_offset, - constraint, - ); + //Layout cursor + let cursor = layout_cursor( + // grid, + cx.text_layout_cache, + &layout, + content.cursor.point, + content.display_offset, + constraint, + ); - (cursor, cells, rects, highlights) - }); + (cursor, cells, rects, highlights) + }); //Select background color let background_color = if self.modal { - tcx.terminal_theme.colors.modal_background + layout.terminal_theme.colors.modal_background } else { - tcx.terminal_theme.colors.background + layout.terminal_theme.colors.background }; //Done! @@ -328,12 +380,10 @@ impl Element for TerminalEl { constraint.max, LayoutState { cells, - line_height: tcx.line_height, - em_width: tcx.cell_width, cursor, background_color, - selection_color: tcx.selection_color, - cur_size, + selection_color: layout.selection_color, + size: layout.size, rects, highlights, }, @@ -358,10 +408,10 @@ impl Element for TerminalEl { let clip_bounds = Some(visible_bounds); cx.paint_layer(clip_bounds, |cx| { - let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); + let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.); //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse - self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.cur_size, cx); + self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.size, cx); cx.paint_layer(clip_bounds, |cx| { //Start with a background color @@ -380,7 +430,7 @@ impl Element for TerminalEl { //Draw Selection cx.paint_layer(clip_bounds, |cx| { let start_y = layout.highlights.get(0).map(|highlight| { - origin.y() + highlight.line_index as f32 * layout.line_height.0 + origin.y() + highlight.line_index as f32 * layout.size.line_height }); if let Some(y) = start_y { @@ -394,11 +444,11 @@ impl Element for TerminalEl { let hr = HighlightedRange { start_y: y, //Need to change this - line_height: layout.line_height.0, + line_height: layout.size.line_height, lines: range_lines, color: layout.selection_color, //Copied from editor. TODO: move to theme or something - corner_radius: 0.15 * layout.line_height.0, + corner_radius: 0.15 * layout.size.line_height, }; hr.paint(bounds, cx.scene); } @@ -436,7 +486,7 @@ impl Element for TerminalEl { .contains_point(*position) .then(|| { let vertical_scroll = - (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER; + (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER; self.connection .upgrade(cx.app) @@ -487,7 +537,7 @@ impl Element for TerminalEl { fn layout_cursor( // grid: &Grid, text_layout_cache: &TextLayoutCache, - tcx: &TerminalLayoutTheme, + tcx: &TerminalLayoutData, cursor_point: Point, display_offset: usize, constraint: SizeConstraint, @@ -497,22 +547,22 @@ fn layout_cursor( cursor_point.line.0 as usize, cursor_point.column.0 as usize, display_offset, - &tcx.line_height, - &tcx.cell_width, - (constraint.max.y() / &tcx.line_height.0) as usize, //TODO + tcx.size.line_height, + tcx.size.cell_width, + (constraint.max.y() / tcx.size.line_height) as usize, //TODO &cursor_text, ) .map(move |(cursor_position, block_width)| { let block_width = if block_width != 0.0 { block_width } else { - tcx.cell_width.0 + tcx.size.cell_width }; Cursor::new( cursor_position, block_width, - tcx.line_height.0, + tcx.size.line_height, tcx.terminal_theme.colors.cursor, CursorShape::Block, Some(cursor_text.clone()), @@ -524,7 +574,7 @@ fn layout_cursor_text( // grid: &Grid, _cursor_point: Point, text_layout_cache: &TextLayoutCache, - tcx: &TerminalLayoutTheme, + tcx: &TerminalLayoutData, ) -> Line { let cursor_text = " "; //grid[cursor_point.line][cursor_point.column].c.to_string(); @@ -545,30 +595,41 @@ fn layout_cursor_text( pub fn mouse_to_cell_data( pos: Vector2F, origin: Vector2F, - cur_size: SizeInfo, + cur_size: TerminalDimensions, display_offset: usize, ) -> (Point, alacritty_terminal::index::Direction) { - let relative_pos = relative_pos(pos, origin); - let point = grid_cell(&relative_pos, cur_size, display_offset); - let side = cell_side(&relative_pos, cur_size); - (point, side) -} + let pos = pos.sub(origin); + let point = { + let col = pos.x() / cur_size.cell_width; //TODO: underflow... + let col = min(GridCol(col as usize), cur_size.last_column()); -///Configures a size info object from the given information. -fn make_new_size( - constraint: SizeConstraint, - cell_width: &CellWidth, - line_height: &LineHeight, -) -> SizeInfo { - SizeInfo::new( - constraint.max.x() - cell_width.0, - constraint.max.y(), - cell_width.0, - line_height.0, - 0., - 0., - false, - ) + let line = pos.y() / cur_size.line_height; + let line = min(line as i32, cur_size.bottommost_line().0); + + Point::new(GridLine(line - display_offset as i32), col) + }; + + //Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() + let side = { + let x = pos.0.x() as usize; + let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize; + let half_cell_width = (cur_size.cell_width / 2.0) as usize; + + let additional_padding = + (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width; + let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding; + //Width: Pixels or columns? + if cell_x > half_cell_width + // Edge case when mouse leaves the window. + || x as f32 >= end_of_grid + { + Side::Right + } else { + Side::Left + } + }; + + (point, side) } fn layout_grid( @@ -686,23 +747,23 @@ fn get_cursor_shape( line: usize, line_index: usize, display_offset: usize, - line_height: &LineHeight, - cell_width: &CellWidth, + line_height: f32, + cell_width: f32, total_lines: usize, text_fragment: &Line, ) -> Option<(Vector2F, f32)> { let cursor_line = line + display_offset; if cursor_line <= total_lines { let cursor_width = if text_fragment.width() == 0. { - cell_width.0 + cell_width } else { text_fragment.width() }; Some(( vec2f( - line_index as f32 * cell_width.0, - cursor_line as f32 * line_height.0, + line_index as f32 * cell_width, + cursor_line as f32 * line_height, ), cursor_width, )) @@ -737,128 +798,6 @@ fn cell_style( } } -// fn attach_mouse_handlers( -// origin: Vector2F, -// cur_size: SizeInfo, -// view_id: usize, -// terminal_mutex: &Arc>>, -// visible_bounds: RectF, -// cx: &mut PaintContext, -// ) { -// let click_mutex = terminal_mutex.clone(); -// let drag_mutex = terminal_mutex.clone(); -// let mouse_down_mutex = terminal_mutex.clone(); - -// cx.scene.push_mouse_region( -// MouseRegion::new(view_id, None, visible_bounds) -// .on_down( -// MouseButton::Left, -// move |MouseButtonEvent { position, .. }, _| { -// let mut term = mouse_down_mutex.lock(); - -// let (point, side) = mouse_to_cell_data( -// position, -// origin, -// cur_size, -// term.renderable_content().display_offset, -// ); -// term.selection = Some(Selection::new(SelectionType::Simple, point, side)) -// }, -// ) -// .on_click( -// MouseButton::Left, -// move |MouseButtonEvent { -// position, -// click_count, -// .. -// }, -// cx| { -// let mut term = click_mutex.lock(); - -// let (point, side) = mouse_to_cell_data( -// position, -// origin, -// cur_size, -// term.renderable_content().display_offset, -// ); - -// let selection_type = match click_count { -// 0 => return, //This is a release -// 1 => Some(SelectionType::Simple), -// 2 => Some(SelectionType::Semantic), -// 3 => Some(SelectionType::Lines), -// _ => None, -// }; - -// let selection = selection_type -// .map(|selection_type| Selection::new(selection_type, point, side)); - -// term.selection = selection; -// cx.focus_parent_view(); -// cx.notify(); -// }, -// ) -// .on_drag( -// MouseButton::Left, -// move |_, MouseMovedEvent { position, .. }, cx| { -// let mut term = drag_mutex.lock(); - -// let (point, side) = mouse_to_cell_data( -// position, -// origin, -// cur_size, -// term.renderable_content().display_offset, -// ); - -// if let Some(mut selection) = term.selection.take() { -// selection.update(point, side); -// term.selection = Some(selection); -// } - -// cx.notify(); -// }, -// ), -// ); -// } - -///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() -fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side { - let x = pos.0.x() as usize; - let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize; - let half_cell_width = (cur_size.cell_width() / 2.0) as usize; - - let additional_padding = - (cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width(); - let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding; - - if cell_x > half_cell_width - // Edge case when mouse leaves the window. - || x as f32 >= end_of_grid - { - Side::Right - } else { - Side::Left - } -} - -///Copied (with modifications) from alacritty/src/event.rs > Mouse::point() -///Position is a pane-relative position. That means the top left corner of the mouse -///Region should be (0,0) -fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point { - let pos = pos.0; - let col = pos.x() / cur_size.cell_width(); //TODO: underflow... - let col = min(GridCol(col as usize), cur_size.last_column()); - - let line = pos.y() / cur_size.cell_height(); - let line = min(line as i32, cur_size.bottommost_line().0); - - //when clicking, need to ADD to get to the top left cell - //e.g. total_lines - viewport_height, THEN subtract display offset - //0 -> total_lines - viewport_height - display_offset + mouse_line - - Point::new(GridLine(line - display_offset as i32), col) -} - mod test { #[test] @@ -872,14 +811,10 @@ mod test { let origin_x = 10.; let origin_y = 20.; - let cur_size = alacritty_terminal::term::SizeInfo::new( - term_width, - term_height, + let cur_size = crate::terminal_element::TerminalDimensions::new( cell_width, line_height, - 0., - 0., - false, + gpui::geometry::vector::vec2f(term_width, term_height), ); let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y); diff --git a/crates/terminal/src/terminal_element/terminal_layout_context.rs b/crates/terminal/src/terminal_element/terminal_layout_context.rs index edfe71c659..9484ef0824 100644 --- a/crates/terminal/src/terminal_element/terminal_layout_context.rs +++ b/crates/terminal/src/terminal_element/terminal_layout_context.rs @@ -1,24 +1,24 @@ use super::*; -pub struct TerminalLayoutTheme<'a> { - pub line_height: LineHeight, - pub cell_width: CellWidth, +pub struct TerminalLayoutData<'a> { pub text_style: TextStyle, pub selection_color: Color, pub terminal_theme: &'a TerminalStyle, + pub size: TerminalDimensions, } -impl<'a> TerminalLayoutTheme<'a> { - pub fn new(settings: &'a Settings, font_cache: &FontCache) -> Self { +impl<'a> TerminalLayoutData<'a> { + pub fn new(settings: &'a Settings, font_cache: &FontCache, constraint: Vector2F) -> Self { let text_style = Self::make_text_style(font_cache, &settings); - let line_height = LineHeight(font_cache.line_height(text_style.font_size)); - let cell_width = CellWidth(font_cache.em_advance(text_style.font_id, text_style.font_size)); let selection_color = settings.theme.editor.selection.selection; let terminal_theme = &settings.theme.terminal; - TerminalLayoutTheme { - line_height, - cell_width, + let line_height = font_cache.line_height(text_style.font_size); + let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); + let dimensions = TerminalDimensions::new(line_height, cell_width, constraint); + + TerminalLayoutData { + size: dimensions, text_style, selection_color, terminal_theme, diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index 1297d5c963..f6d95106e3 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -1,12 +1,11 @@ use std::time::Duration; -use alacritty_terminal::term::SizeInfo; -use gpui::{AppContext, ModelHandle, ReadModelWith, TestAppContext}; +use gpui::{geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, TestAppContext}; use itertools::Itertools; use crate::{ - connection::TerminalConnection, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, - DEBUG_TERMINAL_WIDTH, + connection::TerminalConnection, terminal_element::TerminalDimensions, DEBUG_CELL_WIDTH, + DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH, }; pub struct TerminalTestContext<'a> { @@ -18,14 +17,10 @@ impl<'a> TerminalTestContext<'a> { pub fn new(cx: &'a mut TestAppContext) -> Self { cx.set_condition_duration(Some(Duration::from_secs(5))); - let size_info = SizeInfo::new( - DEBUG_TERMINAL_WIDTH, - DEBUG_TERMINAL_HEIGHT, + let size_info = TerminalDimensions::new( DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, - 0., - 0., - false, + vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), ); let connection = From e2a9a6f361d9eee458529dd1645323749bd184e2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 19 Jul 2022 16:50:16 -0700 Subject: [PATCH 12/29] Maybe fixed toml --- crates/terminal/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 18159a251a..4ddf1a2fd3 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -8,7 +8,7 @@ path = "src/terminal.rs" doctest = false [dependencies] -alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "90647e167c036530176dc5b579222c5886286f63"} +alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "e9b864860ec79cc1b70042aafce100cdd6985a0a"} editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } From 8e5ed9dbd283dfdbc6fbba32d0b684f7a9ca746f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 19 Jul 2022 16:50:24 -0700 Subject: [PATCH 13/29] Maybe fixed toml --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index acb0bec980..844ad59f7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ dependencies = [ [[package]] name = "alacritty_config_derive" version = "0.1.0" -source = "git+https://github.com/zed-industries/alacritty?rev=90647e167c036530176dc5b579222c5886286f63#90647e167c036530176dc5b579222c5886286f63" +source = "git+https://github.com/zed-industries/alacritty?rev=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a" dependencies = [ "proc-macro2", "quote", @@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" version = "0.17.0-dev" -source = "git+https://github.com/zed-industries/alacritty?rev=90647e167c036530176dc5b579222c5886286f63#90647e167c036530176dc5b579222c5886286f63" +source = "git+https://github.com/zed-industries/alacritty?rev=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a" dependencies = [ "alacritty_config_derive", "base64 0.13.0", From b493bafb4813c30df65055d1d28f889a044bcd75 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 20 Jul 2022 10:34:00 -0700 Subject: [PATCH 14/29] Fixed failing test --- crates/terminal/src/terminal_element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 249bf90db7..4a92f30365 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -812,8 +812,8 @@ mod test { let origin_y = 20.; let cur_size = crate::terminal_element::TerminalDimensions::new( - cell_width, line_height, + cell_width, gpui::geometry::vector::vec2f(term_width, term_height), ); From 9b6df1fb6139bfc570926d458ab89ae94aad506a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 20 Jul 2022 12:41:04 -0700 Subject: [PATCH 15/29] Checkpoint, this commit does not compile --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 1 + crates/terminal/src/connection.rs | 140 +++++++++++++----- crates/terminal/src/modal.rs | 6 +- crates/terminal/src/terminal.rs | 16 +- .../src/tests/terminal_test_context.rs | 2 +- 6 files changed, 116 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 844ad59f7a..8e9e976efd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5368,6 +5368,7 @@ dependencies = [ "shellexpand", "smallvec", "theme", + "thiserror", "util", "workspace", ] diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 4ddf1a2fd3..03c6a26b7d 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -25,6 +25,7 @@ dirs = "4.0.0" shellexpand = "2.1.0" libc = "0.2" anyhow = "1" +thiserror = "1.0" [dev-dependencies] diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index df09496f5c..2672a68457 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -13,12 +13,14 @@ use alacritty_terminal::{ tty::{self, setup_env}, Term, }; +use anyhow::{bail, Result}; use futures::{ - channel::mpsc::{unbounded, UnboundedSender}, + channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, StreamExt, }; use settings::{Settings, Shell}; -use std::{collections::HashMap, path::PathBuf, sync::Arc}; +use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc}; +use thiserror::Error; use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; @@ -52,23 +54,84 @@ impl EventListener for ZedListener { } } -pub enum TerminalConnection { - Connected(Terminal), - Disconnected { - directory: Option, - shell: Option, - error: Option, - }, +#[derive(Error, Debug)] +pub struct TerminalError { + directory: Option, + shell: Option, + source: std::io::Error, } -impl TerminalConnection { +impl Display for TerminalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let dir_string: String = self + .directory + .map(|path| { + match path + .into_os_string() + .into_string() + .map_err(|os_str| format!(" {}", os_str.to_string_lossy())) + { + Ok(s) => s, + Err(s) => s, + } + }) + .unwrap_or_else(|| { + let default_dir = + dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy()); + match default_dir { + Some(dir) => format!(" {}", dir), + None => "".to_string(), + } + }); + + let shell = self + .shell + .map(|shell| match shell { + Shell::System => { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + + match pw { + Some(pw) => format!(" {}", pw.shell), + None => "".to_string(), + } + } + Shell::Program(s) => s, + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + }) + .unwrap_or_else(|| { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + match pw { + Some(pw) => { + format!(" {}", pw.shell) + } + None => { + " {}".to_string() + } + } + }); + + write!( + f, + "Working directory: {} Shell command: `{}`, IOError: {}", + dir_string, shell, self.source + ) + } +} + +pub struct DisconnectedPTY { + terminal: Terminal, + events_rx: UnboundedReceiver, +} + +impl DisconnectedPTY { pub fn new( working_directory: Option, shell: Option, env: Option>, initial_size: TerminalDimensions, - cx: &mut ModelContext, - ) -> TerminalConnection { + ) -> Result { let pty_config = { let alac_shell = shell.clone().and_then(|shell| match shell { Shell::System => None, @@ -107,11 +170,11 @@ impl TerminalConnection { let pty = match tty::new(&pty_config, initial_size.into(), None) { Ok(pty) => pty, Err(error) => { - return TerminalConnection::Disconnected { + bail!(TerminalError { directory: working_directory, shell, - error: Some(error), - }; + source: error, + }); } }; @@ -149,20 +212,20 @@ impl TerminalConnection { associated_directory: working_directory, }; + Ok(DisconnectedPTY { + terminal, + events_rx, + }) + } + + pub fn connect(self, cx: &mut ModelContext) -> Terminal { cx.spawn_weak(|this, mut cx| async move { //Listen for terminal events - while let Some(event) = events_rx.next().await { + while let Some(event) = self.events_rx.next().await { match this.upgrade(&cx) { Some(this) => { this.update(&mut cx, |this, cx| { - match this { - TerminalConnection::Connected(conn) => { - conn.process_terminal_event(event, cx) - } - //There should never be a state where the terminal is disconnected - //And receiving events from the pty - TerminalConnection::Disconnected { .. } => unreachable!(), - } + this.process_terminal_event(event, cx); cx.notify(); }); @@ -173,14 +236,7 @@ impl TerminalConnection { }) .detach(); - TerminalConnection::Connected(terminal) - } - - pub fn get_terminal(&self) -> Option<&Terminal> { - match self { - TerminalConnection::Connected(conn) => Some(&conn), - TerminalConnection::Disconnected { .. } => None, - } + self.terminal } } @@ -196,7 +252,7 @@ impl Terminal { fn process_terminal_event( &mut self, event: alacritty_terminal::event::Event, - cx: &mut ModelContext, + cx: &mut ModelContext, ) { match event { // TODO: Handle is_self_focused in subscription on terminal view @@ -361,21 +417,23 @@ impl Terminal { } } -impl Drop for TerminalConnection { +impl Drop for DisconnectedPTY { fn drop(&mut self) { - match self { - TerminalConnection::Connected(conn) => { - conn.pty_tx.0.send(Msg::Shutdown).ok(); - } - _ => {} - }; + self.terminal.pty_tx.0.send(Msg::Shutdown).ok(); } } -impl Entity for TerminalConnection { +impl Drop for Terminal { + fn drop(&mut self) { + self.pty_tx.0.send(Msg::Shutdown).ok(); + } +} + +impl Entity for Terminal { type Event = Event; } +//TODO Move this around mod alacritty_unix { use alacritty_terminal::config::Program; use gpui::anyhow::{bail, Result}; diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 91d2d9ff73..cabfc5b44c 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -1,10 +1,10 @@ use gpui::{ModelHandle, ViewContext}; use workspace::Workspace; -use crate::{get_wd_for_workspace, DeployModal, Event, TerminalConnection, TerminalView}; +use crate::{connection::Terminal, get_wd_for_workspace, DeployModal, Event, TerminalView}; #[derive(Debug)] -struct StoredConnection(ModelHandle); +struct StoredConnection(ModelHandle); pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext) { // Pull the terminal connection out of the global if it has been stored @@ -46,7 +46,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon pub fn on_event( workspace: &mut Workspace, - _: ModelHandle, + _: ModelHandle, event: &Event, cx: &mut ViewContext, ) { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index fc35d0eeea..67e18409c7 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -3,7 +3,7 @@ pub mod connection; mod modal; pub mod terminal_element; -use connection::{Event, TerminalConnection}; +use connection::{DisconnectedPTY, Event, Terminal, TerminalError}; use dirs::home_dir; use gpui::{ actions, elements::*, geometry::vector::vec2f, keymap::Keystroke, AppContext, ClipboardItem, @@ -64,7 +64,7 @@ pub fn init(cx: &mut MutableAppContext) { ///A terminal view, maintains the PTY's file handles and communicates with the terminal pub struct TerminalView { - connection: ModelHandle, + connection: Result, TerminalError>, has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, @@ -94,14 +94,20 @@ impl TerminalView { (shell, envs) }; - let connection = cx - .add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx)); + let connection = DisconnectedPTY::new(working_directory, shell, envs, size_info) + .map(|pty| cx.add_model(|cx| pty.connect(cx))) + .map_err(|err| { + match err.downcast::() { + Ok(err) => err, + Err(_) => unreachable!(), //This should never happen + } + }); TerminalView::from_connection(connection, modal, cx) } fn from_connection( - connection: ModelHandle, + connection: Result, TerminalError>, modal: bool, cx: &mut ViewContext, ) -> TerminalView { diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index f6d95106e3..ec967fb183 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -24,7 +24,7 @@ impl<'a> TerminalTestContext<'a> { ); let connection = - cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx)); + cx.add_model(|cx| TerminalConnection::new_tty(None, None, None, size_info, cx)); TerminalTestContext { cx, connection } } From 061dde5a9bd98462e4dba41afc5c4182739f93b4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 20 Jul 2022 16:48:40 -0700 Subject: [PATCH 16/29] Compiling with new result based error handling --- crates/terminal/src/connection.rs | 20 +- crates/terminal/src/modal.rs | 18 +- crates/terminal/src/terminal.rs | 181 +++++++++++------- crates/terminal/src/terminal_element.rs | 87 ++++----- .../terminal_layout_context.rs | 3 +- .../src/tests/terminal_test_context.rs | 42 ++-- 6 files changed, 192 insertions(+), 159 deletions(-) diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index 2672a68457..48160cd877 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -56,15 +56,16 @@ impl EventListener for ZedListener { #[derive(Error, Debug)] pub struct TerminalError { - directory: Option, - shell: Option, - source: std::io::Error, + pub directory: Option, + pub shell: Option, + pub source: std::io::Error, } impl Display for TerminalError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let dir_string: String = self .directory + .clone() .map(|path| { match path .into_os_string() @@ -77,7 +78,7 @@ impl Display for TerminalError { }) .unwrap_or_else(|| { let default_dir = - dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy()); + dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string()); match default_dir { Some(dir) => format!(" {}", dir), None => "".to_string(), @@ -86,6 +87,7 @@ impl Display for TerminalError { let shell = self .shell + .clone() .map(|shell| match shell { Shell::System => { let mut buf = [0; 1024]; @@ -160,7 +162,7 @@ impl DisconnectedPTY { setup_env(&config); //Spawn a task so the Alacritty EventLoop can communicate with us in a view context - let (events_tx, mut events_rx) = unbounded(); + let (events_tx, events_rx) = unbounded(); //Set up the terminal... let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone())); @@ -218,7 +220,7 @@ impl DisconnectedPTY { }) } - pub fn connect(self, cx: &mut ModelContext) -> Terminal { + pub fn connect(mut self, cx: &mut ModelContext) -> Terminal { cx.spawn_weak(|this, mut cx| async move { //Listen for terminal events while let Some(event) = self.events_rx.next().await { @@ -417,12 +419,6 @@ impl Terminal { } } -impl Drop for DisconnectedPTY { - fn drop(&mut self) { - self.terminal.pty_tx.0.send(Msg::Shutdown).ok(); - } -} - impl Drop for Terminal { fn drop(&mut self) { self.pty_tx.0.send(Msg::Shutdown).ok(); diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index cabfc5b44c..00f338825b 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -16,7 +16,13 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon if let Some(StoredConnection(stored_connection)) = possible_connection { // Create a view from the stored connection workspace.toggle_modal(cx, |_, cx| { - cx.add_view(|cx| TerminalView::from_connection(stored_connection.clone(), true, cx)) + cx.add_view(|cx| { + TerminalView::from_connection( + crate::TerminalConnection(Ok(stored_connection.clone())), + true, + cx, + ) + }) }); cx.set_global::>(Some(StoredConnection( stored_connection.clone(), @@ -28,7 +34,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon let this = cx.add_view(|cx| TerminalView::new(wd, true, cx)); - let connection_handle = this.read(cx).connection.clone(); + let connection_handle = this.read(cx).connection.0.as_ref().unwrap().clone(); cx.subscribe(&connection_handle, on_event).detach(); //Set the global immediately, in case the user opens the command palette cx.set_global::>(Some(StoredConnection( @@ -36,7 +42,13 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon ))); this }) { - let connection = closed_terminal_handle.read(cx).connection.clone(); + let connection = closed_terminal_handle + .read(cx) + .connection + .0 + .as_ref() + .unwrap() + .clone(); cx.set_global(Some(StoredConnection(connection))); } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 67e18409c7..28943a1fbb 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -15,7 +15,7 @@ use project::{LocalWorktree, Project, ProjectPath}; use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::path::{Path, PathBuf}; -use terminal_element::TerminalDimensions; +use terminal_element::{terminal_layout_context::TerminalLayoutData, TerminalDimensions}; use workspace::{Item, Workspace}; @@ -62,9 +62,12 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(TerminalView::clear); } +//New Type to make terminal connection's easier +struct TerminalConnection(Result, TerminalError>); + ///A terminal view, maintains the PTY's file handles and communicates with the terminal pub struct TerminalView { - connection: Result, TerminalError>, + connection: TerminalConnection, has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, @@ -87,12 +90,9 @@ impl TerminalView { vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), ); - let (shell, envs) = { - let settings = cx.global::(); - let shell = settings.terminal_overrides.shell.clone(); - let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. - (shell, envs) - }; + let settings = cx.global::(); + let shell = settings.terminal_overrides.shell.clone(); + let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. let connection = DisconnectedPTY::new(working_directory, shell, envs, size_info) .map(|pty| cx.add_model(|cx| pty.connect(cx))) @@ -103,35 +103,36 @@ impl TerminalView { } }); - TerminalView::from_connection(connection, modal, cx) + TerminalView::from_connection(TerminalConnection(connection), modal, cx) } fn from_connection( - connection: Result, TerminalError>, + connection: TerminalConnection, modal: bool, cx: &mut ViewContext, ) -> TerminalView { - cx.observe(&connection, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&connection, |this, _, event, cx| match event { - Event::Wakeup => { - if cx.is_self_focused() { - cx.notify() - } else { - this.has_new_content = true; - cx.emit(Event::TitleChanged); - } + match connection.0.as_ref() { + Ok(conn) => { + cx.observe(conn, |_, _, cx| cx.notify()).detach(); + cx.subscribe(conn, |this, _, event, cx| match event { + Event::Wakeup => { + if cx.is_self_focused() { + cx.notify() + } else { + this.has_new_content = true; + cx.emit(Event::TitleChanged); + } + } + Event::Bell => { + this.has_bell = true; + cx.emit(Event::TitleChanged); + } + _ => cx.emit(*event), + }) + .detach(); } - Event::Bell => { - this.has_bell = true; - cx.emit(Event::TitleChanged); - } - // Event::Input => { - // this.has_bell = false; - // cx.emit(Event::TitleChanged); - // } - _ => cx.emit(*event), - }) - .detach(); + Err(_) => { /* Leave it as is */ } + } TerminalView { connection, @@ -146,13 +147,6 @@ impl TerminalView { cx.emit(Event::TitleChanged); } - fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { - self.connection - .read(cx) - .get_terminal() - .map(|term| term.clear()); - } - ///Create a new Terminal in the current working directory or the user's home directory fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let wd = get_wd_for_workspace(workspace, cx); @@ -160,63 +154,85 @@ impl TerminalView { workspace.add_item(Box::new(view), cx); } + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { + self.connection + .0 + .as_ref() + .map(|term_handle| term_handle.read(cx).clear()) + .ok(); + } + ///Attempt to paste the clipboard into the terminal fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { self.connection - .read(cx) - .get_terminal() - .and_then(|term| term.copy()) - .map(|text| cx.write_to_clipboard(ClipboardItem::new(text))); + .0 + .as_ref() + .map(|handle| handle.read(cx)) + .map(|term| term.copy()) + .map(|text| text.map(|text| cx.write_to_clipboard(ClipboardItem::new(text)))) + .ok(); } ///Attempt to paste the clipboard into the terminal fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { cx.read_from_clipboard().map(|item| { self.connection - .read(cx) - .get_terminal() - .map(|term| term.paste(item.text())); + .0 + .as_ref() + .map(|handle| handle.read(cx)) + .map(|term| term.paste(item.text())) + .ok(); }); } ///Synthesize the keyboard event corresponding to 'up' fn up(&mut self, _: &Up, cx: &mut ViewContext) { self.connection - .read(cx) - .get_terminal() - .map(|term| term.try_keystroke(&Keystroke::parse("up").unwrap())); + .0 + .as_ref() + .map(|handle| handle.read(cx)) + .map(|term| term.try_keystroke(&Keystroke::parse("up").unwrap())) + .ok(); } ///Synthesize the keyboard event corresponding to 'down' fn down(&mut self, _: &Down, cx: &mut ViewContext) { self.connection - .read(cx) - .get_terminal() - .map(|term| term.try_keystroke(&Keystroke::parse("down").unwrap())); + .0 + .as_ref() + .map(|handle| handle.read(cx)) + .map(|term| term.try_keystroke(&Keystroke::parse("down").unwrap())) + .ok(); } ///Synthesize the keyboard event corresponding to 'ctrl-c' fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { self.connection - .read(cx) - .get_terminal() - .map(|term| term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap())); + .0 + .as_ref() + .map(|handle| handle.read(cx)) + .map(|term| term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap())) + .ok(); } ///Synthesize the keyboard event corresponding to 'escape' fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { self.connection - .read(cx) - .get_terminal() - .map(|term| term.try_keystroke(&Keystroke::parse("escape").unwrap())); + .0 + .as_ref() + .map(|handle| handle.read(cx)) + .map(|term| term.try_keystroke(&Keystroke::parse("escape").unwrap())) + .ok(); } ///Synthesize the keyboard event corresponding to 'enter' fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { self.connection - .read(cx) - .get_terminal() - .map(|term| term.try_keystroke(&Keystroke::parse("enter").unwrap())); + .0 + .as_ref() + .map(|handle| handle.read(cx)) + .map(|term| term.try_keystroke(&Keystroke::parse("enter").unwrap())) + .ok(); } } @@ -226,9 +242,34 @@ impl View for TerminalView { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let element = { - let connection_handle = self.connection.clone().downgrade(); - TerminalEl::new(cx.handle(), connection_handle, self.modal).contained() + let element = match self.connection.0.as_ref() { + Ok(handle) => { + let connection_handle = handle.clone().downgrade(); + TerminalEl::new(cx.handle(), connection_handle, self.modal).contained() + } + Err(e) => { + let settings = cx.global::(); + let style = TerminalLayoutData::make_text_style(cx.font_cache(), settings); + + Flex::column() + .with_child( + Flex::row() + .with_child( + Label::new( + format!( + "Failed to open the terminal. Info: \n{}", + e.to_string() + ), + style, + ) + .boxed(), + ) + .aligned() + .boxed(), + ) + .aligned() + .contained() + } }; if self.modal { @@ -261,9 +302,9 @@ impl Item for TerminalView { tab_theme: &theme::Tab, cx: &gpui::AppContext, ) -> ElementBox { - let title = match self.connection.read(cx) { - TerminalConnection::Connected(conn) => conn.title.clone(), - TerminalConnection::Disconnected { .. } => "Terminal".to_string(), //TODO ask nate about htis + let title = match self.connection.0.as_ref() { + Ok(handle) => handle.read(cx).title.clone(), + Err(_) => "Terminal".to_string(), }; Flex::row() @@ -281,12 +322,10 @@ impl Item for TerminalView { //Directory of the terminal from outside the shell. There might be //solutions to this, but they are non-trivial and require more IPC - let wd = self - .connection - .read(cx) - .get_terminal() - .and_then(|term| term.associated_directory.clone()) - .clone(); + let wd = match self.connection.0.as_ref() { + Ok(term_handle) => term_handle.read(cx).associated_directory.clone(), + Err(e) => e.directory.clone(), + }; Some(TerminalView::new(wd, false, cx)) } diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 4a92f30365..3383359f5c 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -32,7 +32,7 @@ use util::ResultExt; use std::{cmp::min, ops::Range}; use std::{fmt::Debug, ops::Sub}; -use crate::{color_translation::convert_color, connection::TerminalConnection, TerminalView}; +use crate::{color_translation::convert_color, connection::Terminal, TerminalView}; use self::terminal_layout_context::TerminalLayoutData; @@ -44,7 +44,7 @@ const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; ///The GPUI element that paints the terminal. ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalEl { - connection: WeakModelHandle, + connection: WeakModelHandle, view: WeakViewHandle, modal: bool, } @@ -228,7 +228,7 @@ pub struct LayoutState { impl TerminalEl { pub fn new( view: WeakViewHandle, - connection: WeakModelHandle, + connection: WeakModelHandle, modal: bool, ) -> TerminalEl { TerminalEl { @@ -255,19 +255,17 @@ impl TerminalEl { MouseButton::Left, move |MouseButtonEvent { position, .. }, cx| { if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) { - conn_handle.update(cx.app, |connection, cx| { - connection.get_terminal().map(|terminal| { - let (point, side) = mouse_to_cell_data( - position, - origin, - cur_size, - terminal.get_display_offset(), - ); + conn_handle.update(cx.app, |terminal, cx| { + let (point, side) = mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); - terminal.mouse_down(point, side); + terminal.mouse_down(point, side); - cx.notify(); - }); + cx.notify(); }) } }, @@ -282,19 +280,17 @@ impl TerminalEl { cx| { cx.focus_parent_view(); if let Some(conn_handle) = click_connection.upgrade(cx.app) { - conn_handle.update(cx.app, |connection, cx| { - connection.get_terminal().map(|terminal| { - let (point, side) = mouse_to_cell_data( - position, - origin, - cur_size, - terminal.get_display_offset(), - ); + conn_handle.update(cx.app, |terminal, cx| { + let (point, side) = mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); - terminal.click(point, side, click_count); + terminal.click(point, side, click_count); - cx.notify(); - }) + cx.notify(); }); } }, @@ -303,19 +299,17 @@ impl TerminalEl { MouseButton::Left, move |_, MouseMovedEvent { position, .. }, cx| { if let Some(conn_handle) = drag_connection.upgrade(cx.app) { - conn_handle.update(cx.app, |connection, cx| { - connection.get_terminal().map(|terminal| { - let (point, side) = mouse_to_cell_data( - position, - origin, - cur_size, - terminal.get_display_offset(), - ); + conn_handle.update(cx.app, |terminal, cx| { + let (point, side) = mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); - terminal.drag(point, side); + terminal.drag(point, side); - cx.notify() - }); + cx.notify() }); } }, @@ -336,13 +330,7 @@ impl Element for TerminalEl { let layout = TerminalLayoutData::new(cx.global::(), &cx.font_cache(), constraint.max); - let terminal = self - .connection - .upgrade(cx) - .unwrap() - .read(cx) - .get_terminal() - .unwrap(); + let terminal = self.connection.upgrade(cx).unwrap().read(cx); let (cursor, cells, rects, highlights) = terminal.render_lock(Some(layout.size.clone()), |content| { @@ -488,12 +476,11 @@ impl Element for TerminalEl { let vertical_scroll = (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER; - self.connection - .upgrade(cx.app) - .and_then(|handle| handle.read(cx.app).get_terminal()) - .map(|terminal| { - terminal.scroll(Scroll::Delta(vertical_scroll.round() as i32)); - }); + self.connection.upgrade(cx.app).map(|terminal| { + terminal + .read(cx.app) + .scroll(Scroll::Delta(vertical_scroll.round() as i32)); + }); }) .is_some(), Event::KeyDown(KeyDownEvent { keystroke, .. }) => { @@ -508,7 +495,7 @@ impl Element for TerminalEl { self.connection .upgrade(cx.app) - .and_then(|model_handle| model_handle.read(cx.app).get_terminal()) + .map(|model_handle| model_handle.read(cx.app)) .map(|term| term.try_keystroke(keystroke)) .unwrap_or(false) } diff --git a/crates/terminal/src/terminal_element/terminal_layout_context.rs b/crates/terminal/src/terminal_element/terminal_layout_context.rs index 9484ef0824..6128d17828 100644 --- a/crates/terminal/src/terminal_element/terminal_layout_context.rs +++ b/crates/terminal/src/terminal_element/terminal_layout_context.rs @@ -14,6 +14,7 @@ impl<'a> TerminalLayoutData<'a> { let terminal_theme = &settings.theme.terminal; let line_height = font_cache.line_height(text_style.font_size); + let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); let dimensions = TerminalDimensions::new(line_height, cell_width, constraint); @@ -26,7 +27,7 @@ impl<'a> TerminalLayoutData<'a> { } ///Configures a text style from the current settings. - fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { + pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { // Pull the font family from settings properly overriding let family_id = settings .terminal_overrides diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index ec967fb183..429d397d1f 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -4,13 +4,14 @@ use gpui::{geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, Test use itertools::Itertools; use crate::{ - connection::TerminalConnection, terminal_element::TerminalDimensions, DEBUG_CELL_WIDTH, - DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH, + connection::{DisconnectedPTY, Terminal}, + terminal_element::TerminalDimensions, + DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH, }; pub struct TerminalTestContext<'a> { pub cx: &'a mut TestAppContext, - pub connection: ModelHandle, + pub connection: ModelHandle, } impl<'a> TerminalTestContext<'a> { @@ -23,8 +24,11 @@ impl<'a> TerminalTestContext<'a> { vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), ); - let connection = - cx.add_model(|cx| TerminalConnection::new_tty(None, None, None, size_info, cx)); + let connection = cx.add_model(|cx| { + DisconnectedPTY::new(None, None, None, size_info) + .unwrap() + .connect(cx) + }); TerminalTestContext { cx, connection } } @@ -35,11 +39,8 @@ impl<'a> TerminalTestContext<'a> { { let command = command.to_string(); self.connection.update(self.cx, |connection, _| { - connection.get_terminal().unwrap().write_to_pty(command); - connection - .get_terminal() - .unwrap() - .write_to_pty("\r".to_string()); + connection.write_to_pty(command); + connection.write_to_pty("\r".to_string()); }); self.connection @@ -55,18 +56,15 @@ impl<'a> TerminalTestContext<'a> { }) } - fn grid_as_str(connection: &TerminalConnection) -> String { - connection - .get_terminal() - .unwrap() - .render_lock(None, |content| { - let lines = content.display_iter.group_by(|i| i.point.line.0); - lines - .into_iter() - .map(|(_, line)| line.map(|i| i.c).collect::()) - .collect::>() - .join("\n") - }) + fn grid_as_str(connection: &Terminal) -> String { + connection.render_lock(None, |content| { + let lines = content.display_iter.group_by(|i| i.point.line.0); + lines + .into_iter() + .map(|(_, line)| line.map(|i| i.c).collect::()) + .collect::>() + .join("\n") + }) } } From 741b78a15b46062f227b1f019fd7cdd2d9ace2de Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 20 Jul 2022 17:11:36 -0700 Subject: [PATCH 17/29] I think I'm finished --- crates/terminal/src/modal.rs | 2 +- crates/terminal/src/terminal.rs | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 00f338825b..f272ccb3d4 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -32,7 +32,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { let wd = get_wd_for_workspace(workspace, cx); - let this = cx.add_view(|cx| TerminalView::new(wd, true, cx)); + let this = cx.add_view(|cx| TerminalView::new(wd, true, cx).unwrap()); let connection_handle = this.read(cx).connection.0.as_ref().unwrap().clone(); cx.subscribe(&connection_handle, on_event).detach(); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 28943a1fbb..d47b4fa961 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -82,7 +82,11 @@ impl Entity for TerminalView { impl TerminalView { ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` - fn new(working_directory: Option, modal: bool, cx: &mut ViewContext) -> Self { + fn new( + working_directory: Option, + modal: bool, + cx: &mut ViewContext, + ) -> Option { //The details here don't matter, the terminal will be resized on the first layout let size_info = TerminalDimensions::new( DEBUG_LINE_HEIGHT, @@ -103,7 +107,15 @@ impl TerminalView { } }); - TerminalView::from_connection(TerminalConnection(connection), modal, cx) + if let Ok(_) = connection { + Some(TerminalView::from_connection( + TerminalConnection(connection), + modal, + cx, + )) + } else { + None + } } fn from_connection( @@ -150,8 +162,9 @@ impl TerminalView { ///Create a new Terminal in the current working directory or the user's home directory fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let wd = get_wd_for_workspace(workspace, cx); - let view = cx.add_view(|cx| TerminalView::new(wd, false, cx)); - workspace.add_item(Box::new(view), cx); + if let Some(view) = cx.add_option_view(|cx| TerminalView::new(wd, false, cx)) { + workspace.add_item(Box::new(view), cx); + } } fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { @@ -327,7 +340,7 @@ impl Item for TerminalView { Err(e) => e.directory.clone(), }; - Some(TerminalView::new(wd, false, cx)) + TerminalView::new(wd, false, cx) } fn project_path(&self, _cx: &gpui::AppContext) -> Option { From ee87c8ebdeca764ecb6f08df1ea3bebadc1cb570 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 20 Jul 2022 17:25:21 -0700 Subject: [PATCH 18/29] Render is snappier, less locking, and errors feel nicely modeled --- crates/terminal/src/modal.rs | 1 + crates/terminal/src/terminal.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index f272ccb3d4..0cd0febe78 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -32,6 +32,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { let wd = get_wd_for_workspace(workspace, cx); + //TODO fix this crash let this = cx.add_view(|cx| TerminalView::new(wd, true, cx).unwrap()); let connection_handle = this.read(cx).connection.0.as_ref().unwrap().clone(); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d47b4fa961..0bbad588c5 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -16,7 +16,7 @@ use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::path::{Path, PathBuf}; use terminal_element::{terminal_layout_context::TerminalLayoutData, TerminalDimensions}; - +use util::ResultExt; use workspace::{Item, Workspace}; use crate::terminal_element::TerminalEl; @@ -114,6 +114,7 @@ impl TerminalView { cx, )) } else { + connection.log_err(); None } } From dbedc30abe52682ad212d9189e18950daec90901 Mon Sep 17 00:00:00 2001 From: K Simmons Date: Wed, 13 Jul 2022 19:35:18 -0700 Subject: [PATCH 19/29] WIP --- crates/editor/src/element.rs | 5 ++- crates/editor/src/hover_popover.rs | 64 +++++++++++++++++------------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a2e693fcd3..24a7c490f9 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1335,8 +1335,11 @@ impl Element for EditorElement { .map(|indicator| (newest_selection_head.row(), indicator)); } - hover = view.hover_state.popover.clone().and_then(|hover| { + hover = view.hover_state.info_popover.clone().and_then(|hover| { let (point, rendered) = hover.render(&snapshot, style.clone(), cx); + // The scroll position is a fractional point, the whole number of which represents + // the top of the window in terms of display rows. + // Ensure the hover point is above the scroll position if point.row() >= snapshot.scroll_position().y() as u32 { if line_layouts.len() > (point.row() - start_row) as usize { return Some((point, rendered)); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 7ae9e01b09..a2e99275ac 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -32,6 +32,21 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(hover_at); } +#[derive(Default)] +pub struct HoverState { + pub info_popover: Option, + pub diagnostic_popover: Option, + pub triggered_from: Option, + pub symbol_range: Option>, + pub task: Option>>, +} + +impl HoverState { + pub fn visible(&self) -> bool { + self.info_popover.is_some() + } +} + /// Bindable action which uses the most recent selection head to trigger a hover pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { let head = editor.selections.newest_display(cx).head(); @@ -57,8 +72,8 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext) -> bool { let mut did_hide = false; // only notify the context once - if editor.hover_state.popover.is_some() { - editor.hover_state.popover = None; + if editor.hover_state.info_popover.is_some() { + editor.hover_state.info_popover = None; did_hide = true; cx.notify(); } @@ -167,6 +182,16 @@ fn show_hover( }) }); + if let Some(delay) = delay { + delay.await; + } + + // If there's a diagnostic, assign it on the hover state and notify + let diagnostic = snapshot + .buffer_snapshot + .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false) + .next(); + // Construct new hover popover from hover request let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| { if hover_result.contents.is_empty() { @@ -194,17 +219,13 @@ fn show_hover( }); } - Some(HoverPopover { + Some(InfoPopover { project: project.clone(), anchor: range.start.clone(), contents: hover_result.contents, }) }); - if let Some(delay) = delay { - delay.await; - } - if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { if hover_popover.is_some() { @@ -216,7 +237,7 @@ fn show_hover( cx, ); } - this.hover_state.popover = hover_popover; + this.hover_state.info_popover = hover_popover; cx.notify(); } else { if this.hover_state.visible() { @@ -237,35 +258,21 @@ fn show_hover( editor.hover_state.task = Some(task); } -#[derive(Default)] -pub struct HoverState { - pub popover: Option, - pub triggered_from: Option, - pub symbol_range: Option>, - pub task: Option>>, -} - -impl HoverState { - pub fn visible(&self) -> bool { - self.popover.is_some() - } -} - #[derive(Debug, Clone)] -pub struct HoverPopover { +pub struct InfoPopover { pub project: ModelHandle, pub anchor: Anchor, pub contents: Vec, } -impl HoverPopover { +impl InfoPopover { pub fn render( &self, snapshot: &EditorSnapshot, style: EditorStyle, cx: &mut RenderContext, ) -> (DisplayPoint, ElementBox) { - let element = MouseEventHandler::new::(0, cx, |_, cx| { + let element = MouseEventHandler::new::(0, cx, |_, cx| { let mut flex = Flex::new(Axis::Vertical).scrollable::(1, None, cx); flex.extend(self.contents.iter().map(|content| { let project = self.project.read(cx); @@ -316,6 +323,9 @@ impl HoverPopover { } } +#[derive(Debug, Clone)] +pub struct DiagnosticPopover {} + #[cfg(test)] mod tests { use futures::StreamExt; @@ -382,7 +392,7 @@ mod tests { cx.editor(|editor, _| { assert!(editor.hover_state.visible()); assert_eq!( - editor.hover_state.popover.clone().unwrap().contents, + editor.hover_state.info_popover.clone().unwrap().contents, vec![ HoverBlock { text: "Some basic docs".to_string(), @@ -446,7 +456,7 @@ mod tests { cx.editor(|editor, _| { assert!(editor.hover_state.visible()); assert_eq!( - editor.hover_state.popover.clone().unwrap().contents, + editor.hover_state.info_popover.clone().unwrap().contents, vec![ HoverBlock { text: "Some other basic docs".to_string(), From 95952f0c66a25b34f400b4f6af1a8875a6adb079 Mon Sep 17 00:00:00 2001 From: K Simmons Date: Fri, 15 Jul 2022 19:39:52 -0700 Subject: [PATCH 20/29] working diagnostic popover. Also renamed GoToNextDiagnostic to GoToDiagnostic and adjusted it's action to jump to the popover's diagnostic if it is visible --- assets/keymaps/default.json | 2 +- crates/diagnostics/src/items.rs | 8 +- crates/editor/src/editor.rs | 30 ++-- crates/editor/src/element.rs | 159 ++++++++++++------- crates/editor/src/hover_popover.rs | 226 +++++++++++++++++++-------- crates/text/src/selection.rs | 14 +- crates/theme/src/theme.rs | 3 + crates/zed/src/menus.rs | 2 +- styles/src/styleTree/hoverPopover.ts | 50 ++++-- styles/src/themes/common/base16.ts | 23 ++- styles/src/themes/common/theme.ts | 3 + 11 files changed, 355 insertions(+), 165 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 11893847ad..92f74907ab 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -190,7 +190,7 @@ "alt-down": "editor::SelectSmallerSyntaxNode", "cmd-u": "editor::UndoSelection", "cmd-shift-U": "editor::RedoSelection", - "f8": "editor::GoToNextDiagnostic", + "f8": "editor::GoToDiagnostic", "shift-f8": "editor::GoToPrevDiagnostic", "f2": "editor::Rename", "f12": "editor::GoToDefinition", diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 81ec57fe54..cc5932a781 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,5 +1,5 @@ use collections::HashSet; -use editor::{Editor, GoToNextDiagnostic}; +use editor::{Editor, GoToDiagnostic}; use gpui::{ elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, @@ -48,10 +48,10 @@ impl DiagnosticIndicator { } } - fn go_to_next_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext) { + fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade(cx)) { editor.update(cx, |editor, cx| { - editor.go_to_diagnostic(editor::Direction::Next, cx); + editor.go_to_diagnostic_impl(editor::Direction::Next, cx); }) } } @@ -202,7 +202,7 @@ impl View for DiagnosticIndicator { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(GoToNextDiagnostic) + cx.dispatch_action(GoToDiagnostic) }) .boxed(), ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 47501192b0..f94da90d31 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -82,9 +82,6 @@ pub struct SelectNext { pub replace_newest: bool, } -#[derive(Clone, PartialEq)] -pub struct GoToDiagnostic(pub Direction); - #[derive(Clone, PartialEq)] pub struct Scroll(pub Vector2F); @@ -138,7 +135,7 @@ actions!( Backspace, Delete, Newline, - GoToNextDiagnostic, + GoToDiagnostic, GoToPrevDiagnostic, Indent, Outdent, @@ -300,7 +297,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::move_to_enclosing_bracket); cx.add_action(Editor::undo_selection); cx.add_action(Editor::redo_selection); - cx.add_action(Editor::go_to_next_diagnostic); + cx.add_action(Editor::go_to_diagnostic); cx.add_action(Editor::go_to_prev_diagnostic); cx.add_action(Editor::go_to_definition); cx.add_action(Editor::page_up); @@ -4564,17 +4561,32 @@ impl Editor { self.selection_history.mode = SelectionHistoryMode::Normal; } - fn go_to_next_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext) { - self.go_to_diagnostic(Direction::Next, cx) + fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { + self.go_to_diagnostic_impl(Direction::Next, cx) } fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext) { - self.go_to_diagnostic(Direction::Prev, cx) + self.go_to_diagnostic_impl(Direction::Prev, cx) } - pub fn go_to_diagnostic(&mut self, direction: Direction, cx: &mut ViewContext) { + pub fn go_to_diagnostic_impl(&mut self, direction: Direction, cx: &mut ViewContext) { let buffer = self.buffer.read(cx).snapshot(cx); let selection = self.selections.newest::(cx); + + // If there is an active Diagnostic Popover. Jump to it's diagnostic instead. + if direction == Direction::Next { + if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() { + let (group_id, jump_to) = popover.activation_info(); + self.activate_diagnostics(group_id, cx); + self.change_selections(Some(Autoscroll::Center), cx, |s| { + let mut new_selection = s.newest_anchor().clone(); + new_selection.collapse_to(jump_to, SelectionGoal::None); + s.select_anchors(vec![new_selection.clone()]); + }); + return; + } + } + let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { active_diagnostics .primary_range diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 24a7c490f9..c3ef8da399 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -41,6 +41,10 @@ use std::{ ops::Range, }; +const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; +const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; +const HOVER_POPOVER_GAP: f32 = 10.; + struct SelectionLayout { head: DisplayPoint, range: Range, @@ -268,8 +272,9 @@ impl EditorElement { } if paint - .hover_bounds - .map_or(false, |hover_bounds| hover_bounds.contains_point(position)) + .hover_popover_bounds + .iter() + .any(|hover_bounds| hover_bounds.contains_point(position)) { return false; } @@ -600,35 +605,78 @@ impl EditorElement { cx.scene.pop_stacking_context(); } - if let Some((position, hover_popover)) = layout.hover.as_mut() { + if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { cx.scene.push_stacking_context(None); // This is safe because we check on layout whether the required row is available let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize]; - let size = hover_popover.size(); + + // Minimum required size: Take the first popover, and add 1.5 times the minimum popover + // height. This is the size we will use to decide whether to render popovers above or below + // the hovered line. + let first_size = hover_popovers[0].size(); + let height_to_reserve = + first_size.y() + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.line_height; + + // Compute Hovered Point let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; - let y = position.row() as f32 * layout.line_height - scroll_top - size.y(); - let mut popover_origin = content_origin + vec2f(x, y); + let y = position.row() as f32 * layout.line_height - scroll_top; + let hovered_point = content_origin + vec2f(x, y); - if popover_origin.y() < 0.0 { - popover_origin.set_y(popover_origin.y() + layout.line_height + size.y()); + paint.hover_popover_bounds.clear(); + + if hovered_point.y() - height_to_reserve > 0.0 { + // There is enough space above. Render popovers above the hovered point + let mut current_y = hovered_point.y(); + for hover_popover in hover_popovers { + let size = hover_popover.size(); + let mut popover_origin = vec2f(hovered_point.x(), current_y - size.y()); + + let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); + if x_out_of_bounds < 0.0 { + popover_origin.set_x(popover_origin.x() + x_out_of_bounds); + } + + hover_popover.paint( + popover_origin, + RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor + cx, + ); + + paint.hover_popover_bounds.push( + RectF::new(popover_origin, hover_popover.size()) + .dilate(Vector2F::new(0., 5.)), + ); + + current_y = popover_origin.y() - HOVER_POPOVER_GAP; + } + } else { + // There is not enough space above. Render popovers below the hovered point + let mut current_y = hovered_point.y() + layout.line_height; + for hover_popover in hover_popovers { + let size = hover_popover.size(); + let mut popover_origin = vec2f(hovered_point.x(), current_y); + + let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); + if x_out_of_bounds < 0.0 { + popover_origin.set_x(popover_origin.x() + x_out_of_bounds); + } + + hover_popover.paint( + popover_origin, + RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor + cx, + ); + + paint.hover_popover_bounds.push( + RectF::new(popover_origin, hover_popover.size()) + .dilate(Vector2F::new(0., 5.)), + ); + + current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP; + } } - let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); - if x_out_of_bounds < 0.0 { - popover_origin.set_x(popover_origin.x() + x_out_of_bounds); - } - - hover_popover.paint( - popover_origin, - RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor - cx, - ); - - paint.hover_bounds = Some( - RectF::new(popover_origin, hover_popover.size()).dilate(Vector2F::new(0., 5.)), - ); - cx.scene.pop_stacking_context(); } @@ -1162,6 +1210,8 @@ impl Element for EditorElement { }); let scroll_position = snapshot.scroll_position(); + // The scroll position is a fractional point, the whole number of which represents + // the top of the window in terms of display rows. let start_row = scroll_position.y() as u32; let scroll_top = scroll_position.y() * line_height; @@ -1335,19 +1385,8 @@ impl Element for EditorElement { .map(|indicator| (newest_selection_head.row(), indicator)); } - hover = view.hover_state.info_popover.clone().and_then(|hover| { - let (point, rendered) = hover.render(&snapshot, style.clone(), cx); - // The scroll position is a fractional point, the whole number of which represents - // the top of the window in terms of display rows. - // Ensure the hover point is above the scroll position - if point.row() >= snapshot.scroll_position().y() as u32 { - if line_layouts.len() > (point.row() - start_row) as usize { - return Some((point, rendered)); - } - } - - None - }); + let visible_rows = start_row..start_row + line_layouts.len() as u32; + hover = view.hover_state.render(&snapshot, &style, visible_rows, cx); }); if let Some((_, context_menu)) = context_menu.as_mut() { @@ -1370,21 +1409,23 @@ impl Element for EditorElement { ); } - if let Some((_, hover)) = hover.as_mut() { - hover.layout( - SizeConstraint { - min: Vector2F::zero(), - max: vec2f( - (120. * em_width) // Default size - .min(size.x() / 2.) // Shrink to half of the editor width - .max(20. * em_width), // Apply minimum width of 20 characters - (16. * line_height) // Default size - .min(size.y() / 2.) // Shrink to half of the editor height - .max(4. * line_height), // Apply minimum height of 4 lines - ), - }, - cx, - ); + if let Some((_, hover_popovers)) = hover.as_mut() { + for hover_popover in hover_popovers.iter_mut() { + hover_popover.layout( + SizeConstraint { + min: Vector2F::zero(), + max: vec2f( + (120. * em_width) // Default size + .min(size.x() / 2.) // Shrink to half of the editor width + .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters + (16. * line_height) // Default size + .min(size.y() / 2.) // Shrink to half of the editor height + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + ), + }, + cx, + ); + } } ( @@ -1409,7 +1450,7 @@ impl Element for EditorElement { selections, context_menu, code_actions_indicator, - hover, + hover_popovers: hover, }, ) } @@ -1434,7 +1475,7 @@ impl Element for EditorElement { gutter_bounds, text_bounds, context_menu_bounds: None, - hover_bounds: None, + hover_popover_bounds: Default::default(), }; self.paint_background(gutter_bounds, text_bounds, layout, cx); @@ -1475,9 +1516,11 @@ impl Element for EditorElement { } } - if let Some((_, hover)) = &mut layout.hover { - if hover.dispatch_event(event, cx) { - return true; + if let Some((_, popover_elements)) = &mut layout.hover_popovers { + for popover_element in popover_elements.iter_mut() { + if popover_element.dispatch_event(event, cx) { + return true; + } } } @@ -1572,7 +1615,7 @@ pub struct LayoutState { selections: Vec<(ReplicaId, Vec)>, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, - hover: Option<(DisplayPoint, ElementBox)>, + hover_popovers: Option<(DisplayPoint, Vec)>, } struct BlockLayout { @@ -1617,7 +1660,7 @@ pub struct PaintState { gutter_bounds: RectF, text_bounds: RectF, context_menu_bounds: Option, - hover_bounds: Option, + hover_popover_bounds: Vec, } impl PaintState { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index a2e99275ac..cd7f6eca10 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -3,9 +3,10 @@ use gpui::{ elements::{Flex, MouseEventHandler, Padding, Text}, impl_internal_actions, platform::CursorStyle, - Axis, Element, ElementBox, ModelHandle, MutableAppContext, RenderContext, Task, ViewContext, + Axis, Element, ElementBox, ModelHandle, MouseButton, MutableAppContext, RenderContext, Task, + ViewContext, }; -use language::Bias; +use language::{Bias, DiagnosticEntry, DiagnosticSeverity}; use project::{HoverBlock, Project}; use settings::Settings; use std::{ops::Range, time::Duration}; @@ -13,7 +14,7 @@ use util::TryFutureExt; use crate::{ display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot, - EditorStyle, + EditorStyle, GoToDiagnostic, RangeToAnchorExt, }; pub const HOVER_DELAY_MILLIS: u64 = 350; @@ -32,21 +33,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(hover_at); } -#[derive(Default)] -pub struct HoverState { - pub info_popover: Option, - pub diagnostic_popover: Option, - pub triggered_from: Option, - pub symbol_range: Option>, - pub task: Option>>, -} - -impl HoverState { - pub fn visible(&self) -> bool { - self.info_popover.is_some() - } -} - /// Bindable action which uses the most recent selection head to trigger a hover pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { let head = editor.selections.newest_display(cx).head(); @@ -69,17 +55,11 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext) -> bool { - let mut did_hide = false; + let did_hide = editor.hover_state.info_popover.take().is_some() + | editor.hover_state.diagnostic_popover.take().is_some(); - // only notify the context once - if editor.hover_state.info_popover.is_some() { - editor.hover_state.info_popover = None; - did_hide = true; - cx.notify(); - } - editor.hover_state.task = None; + editor.hover_state.info_task = None; editor.hover_state.triggered_from = None; - editor.hover_state.symbol_range = None; editor.clear_background_highlights::(cx); @@ -129,8 +109,8 @@ fn show_hover( }; if !ignore_timeout { - if let Some(range) = &editor.hover_state.symbol_range { - if range + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if symbol_range .to_offset(&snapshot.buffer_snapshot) .contains(&multibuffer_offset) { @@ -187,10 +167,37 @@ fn show_hover( } // If there's a diagnostic, assign it on the hover state and notify - let diagnostic = snapshot + let local_diagnostic = snapshot .buffer_snapshot .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false) - .next(); + // Find the entry with the most specific range + .min_by_key(|entry| entry.range.end - entry.range.start) + .map(|entry| DiagnosticEntry { + diagnostic: entry.diagnostic, + range: entry.range.to_anchors(&snapshot.buffer_snapshot), + }); + + // Pull the primary diagnostic out so we can jump to it if the popover is clicked + let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| { + snapshot + .buffer_snapshot + .diagnostic_group::(local_diagnostic.diagnostic.group_id) + .find(|diagnostic| diagnostic.diagnostic.is_primary) + .map(|entry| DiagnosticEntry { + diagnostic: entry.diagnostic, + range: entry.range.to_anchors(&snapshot.buffer_snapshot), + }) + }); + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = + local_diagnostic.map(|local_diagnostic| DiagnosticPopover { + local_diagnostic, + primary_diagnostic, + }); + }); + } // Construct new hover popover from hover request let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| { @@ -213,41 +220,28 @@ fn show_hover( anchor.clone()..anchor.clone() }; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { - this.hover_state.symbol_range = Some(range.clone()); - }); - } - Some(InfoPopover { project: project.clone(), - anchor: range.start.clone(), + symbol_range: range.clone(), contents: hover_result.contents, }) }); if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - if hover_popover.is_some() { + if let Some(hover_popover) = hover_popover.as_ref() { // Highlight the selected symbol using a background highlight - if let Some(range) = this.hover_state.symbol_range.clone() { - this.highlight_background::( - vec![range], - |theme| theme.editor.hover_popover.highlight, - cx, - ); - } - this.hover_state.info_popover = hover_popover; - cx.notify(); + this.highlight_background::( + vec![hover_popover.symbol_range.clone()], + |theme| theme.editor.hover_popover.highlight, + cx, + ); } else { - if this.hover_state.visible() { - // Popover was visible, but now is hidden. Dismiss it - hide_hover(this, cx); - } else { - // Clear selected symbol range for future requests - this.hover_state.symbol_range = None; - } + this.clear_background_highlights::(cx); } + + this.hover_state.info_popover = hover_popover; + cx.notify(); }); } Ok::<_, anyhow::Error>(()) @@ -255,24 +249,70 @@ fn show_hover( .log_err() }); - editor.hover_state.task = Some(task); + editor.hover_state.info_task = Some(task); +} + +#[derive(Default)] +pub struct HoverState { + pub info_popover: Option, + pub diagnostic_popover: Option, + pub triggered_from: Option, + pub info_task: Option>>, +} + +impl HoverState { + pub fn visible(&self) -> bool { + self.info_popover.is_some() || self.diagnostic_popover.is_some() + } + + pub fn render( + &self, + snapshot: &EditorSnapshot, + style: &EditorStyle, + visible_rows: Range, + cx: &mut RenderContext, + ) -> Option<(DisplayPoint, Vec)> { + // If there is a diagnostic, position the popovers based on that. + // Otherwise use the start of the hover range + let anchor = self + .diagnostic_popover + .as_ref() + .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) + .or_else(|| { + self.info_popover + .as_ref() + .map(|info_popover| &info_popover.symbol_range.start) + })?; + let point = anchor.to_display_point(&snapshot.display_snapshot); + + // Don't render if the relevant point isn't on screen + if !self.visible() || !visible_rows.contains(&point.row()) { + return None; + } + + let mut elements = Vec::new(); + + if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { + elements.push(diagnostic_popover.render(style, cx)); + } + if let Some(info_popover) = self.info_popover.as_ref() { + elements.push(info_popover.render(style, cx)); + } + + Some((point, elements)) + } } #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - pub anchor: Anchor, + pub symbol_range: Range, pub contents: Vec, } impl InfoPopover { - pub fn render( - &self, - snapshot: &EditorSnapshot, - style: EditorStyle, - cx: &mut RenderContext, - ) -> (DisplayPoint, ElementBox) { - let element = MouseEventHandler::new::(0, cx, |_, cx| { + pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext) -> ElementBox { + MouseEventHandler::new::(0, cx, |_, cx| { let mut flex = Flex::new(Axis::Vertical).scrollable::(1, None, cx); flex.extend(self.contents.iter().map(|content| { let project = self.project.read(cx); @@ -316,15 +356,63 @@ impl InfoPopover { top: 5., ..Default::default() }) - .boxed(); - - let display_point = self.anchor.to_display_point(&snapshot.display_snapshot); - (display_point, element) + .boxed() } } #[derive(Debug, Clone)] -pub struct DiagnosticPopover {} +pub struct DiagnosticPopover { + local_diagnostic: DiagnosticEntry, + primary_diagnostic: Option>, +} + +impl DiagnosticPopover { + pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext) -> ElementBox { + enum PrimaryDiagnostic {} + + let mut text_style = style.hover_popover.prose.clone(); + text_style.font_size = style.text.font_size; + + let container_style = match self.local_diagnostic.diagnostic.severity { + DiagnosticSeverity::HINT => style.hover_popover.info_container, + DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, + DiagnosticSeverity::WARNING => style.hover_popover.warning_container, + DiagnosticSeverity::ERROR => style.hover_popover.error_container, + _ => style.hover_popover.container, + }; + + let tooltip_style = cx.global::().theme.tooltip.clone(); + + MouseEventHandler::new::(0, cx, |_, _| { + Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style) + .with_soft_wrap(true) + .contained() + .with_style(container_style) + .boxed() + }) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(GoToDiagnostic) + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + 0, + "Go To Diagnostic".to_string(), + Some(Box::new(crate::GoToDiagnostic)), + tooltip_style, + cx, + ) + .boxed() + } + + pub fn activation_info(&self) -> (usize, Anchor) { + let entry = self + .primary_diagnostic + .as_ref() + .unwrap_or(&self.local_diagnostic); + + (entry.diagnostic.group_id, entry.range.start.clone()) + } +} #[cfg(test)] mod tests { diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index fd8d57ca9f..e5acbd21bc 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -54,6 +54,13 @@ impl Selection { goal: self.goal, } } + + pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) { + self.start = point.clone(); + self.end = point; + self.goal = new_goal; + self.reversed = false; + } } impl Selection { @@ -78,13 +85,6 @@ impl Selection { self.goal = new_goal; } - pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) { - self.start = point; - self.end = point; - self.goal = new_goal; - self.reversed = false; - } - pub fn range(&self) -> Range { self.start..self.end } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 2299bc3477..d6961d97b2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -627,6 +627,9 @@ impl<'de> Deserialize<'de> for SyntaxTheme { #[derive(Clone, Deserialize, Default)] pub struct HoverPopover { pub container: ContainerStyle, + pub info_container: ContainerStyle, + pub warning_container: ContainerStyle, + pub error_container: ContainerStyle, pub block_style: ContainerStyle, pub prose: TextStyle, pub highlight: Color, diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index d4d9b401e6..5b95c2b7eb 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -281,7 +281,7 @@ pub fn menus() -> Vec> { MenuItem::Separator, MenuItem::Action { name: "Next Problem", - action: Box::new(editor::GoToNextDiagnostic), + action: Box::new(editor::GoToDiagnostic), }, MenuItem::Action { name: "Previous Problem", diff --git a/styles/src/styleTree/hoverPopover.ts b/styles/src/styleTree/hoverPopover.ts index d6665cacff..aeac9c0166 100644 --- a/styles/src/styleTree/hoverPopover.ts +++ b/styles/src/styleTree/hoverPopover.ts @@ -2,22 +2,48 @@ import Theme from "../themes/common/theme"; import { backgroundColor, border, popoverShadow, text } from "./components"; export default function HoverPopover(theme: Theme) { + let baseContainer = { + background: backgroundColor(theme, "on500"), + cornerRadius: 8, + padding: { + left: 8, + right: 8, + top: 4, + bottom: 4 + }, + shadow: popoverShadow(theme), + border: border(theme, "secondary"), + margin: { + left: -8, + }, + }; + return { - container: { - background: backgroundColor(theme, "on500"), - cornerRadius: 8, - padding: { - left: 8, - right: 8, - top: 4, - bottom: 4, + container: baseContainer, + infoContainer: { + ...baseContainer, + background: backgroundColor(theme, "on500Info"), + border: { + color: theme.ramps.blue(0.2).hex(), + width: 1, }, - shadow: popoverShadow(theme), - border: border(theme, "primary"), - margin: { - left: -8, + }, + warningContainer: { + ...baseContainer, + background: backgroundColor(theme, "on500Warning"), + border: { + color: theme.ramps.yellow(0.2).hex(), + width: 1, }, }, + errorContainer: { + ...baseContainer, + background: backgroundColor(theme, "on500Error"), + border: { + color: theme.ramps.red(0.2).hex(), + width: 1, + } + }, block_style: { padding: { top: 4 }, }, diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index a73ac7f0cf..78856d6191 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -88,16 +88,31 @@ export function createTheme( hovered: withOpacity(sample(ramps.red, 0.5), 0.2), active: withOpacity(sample(ramps.red, 0.5), 0.25), }, + on500Error: { + base: sample(ramps.red, 0.1), + hovered: sample(ramps.red, 0.15), + active: sample(ramps.red, 0.2), + }, warning: { base: withOpacity(sample(ramps.yellow, 0.5), 0.15), hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2), active: withOpacity(sample(ramps.yellow, 0.5), 0.25), }, + on500Warning: { + base: sample(ramps.yellow, 0.1), + hovered: sample(ramps.yellow, 0.15), + active: sample(ramps.yellow, 0.2), + }, info: { base: withOpacity(sample(ramps.blue, 0.5), 0.15), hovered: withOpacity(sample(ramps.blue, 0.5), 0.2), active: withOpacity(sample(ramps.blue, 0.5), 0.25), }, + on500Info: { + base: sample(ramps.blue, 0.1), + hovered: sample(ramps.blue, 0.15), + active: sample(ramps.blue, 0.2), + }, }; const borderColor = { @@ -106,10 +121,10 @@ export function createTheme( muted: sample(ramps.neutral, isLight ? 1 : 3), active: sample(ramps.neutral, isLight ? 4 : 3), onMedia: withOpacity(darkest, 0.1), - ok: withOpacity(sample(ramps.green, 0.5), 0.15), - error: withOpacity(sample(ramps.red, 0.5), 0.15), - warning: withOpacity(sample(ramps.yellow, 0.5), 0.15), - info: withOpacity(sample(ramps.blue, 0.5), 0.15), + ok: sample(ramps.green, 0.3), + error: sample(ramps.red, 0.3), + warning: sample(ramps.yellow, 0.3), + info: sample(ramps.blue, 0.3), }; const textColor = { diff --git a/styles/src/themes/common/theme.ts b/styles/src/themes/common/theme.ts index ac0902d8a2..e01435b846 100644 --- a/styles/src/themes/common/theme.ts +++ b/styles/src/themes/common/theme.ts @@ -79,8 +79,11 @@ export default interface Theme { on500: BackgroundColorSet; ok: BackgroundColorSet; error: BackgroundColorSet; + on500Error: BackgroundColorSet; warning: BackgroundColorSet; + on500Warning: BackgroundColorSet; info: BackgroundColorSet; + on500Info: BackgroundColorSet; }; borderColor: { primary: string; From 02a47d962ef25ef639cd5d51f3df9c57f89930bc Mon Sep 17 00:00:00 2001 From: K Simmons Date: Tue, 19 Jul 2022 15:59:16 -0700 Subject: [PATCH 21/29] add test coverage for diagnostic popover --- crates/editor/src/hover_popover.rs | 145 ++++++++++++++++++++++------- crates/editor/src/test.rs | 81 +++++++++++++--- crates/language/src/buffer.rs | 6 +- 3 files changed, 185 insertions(+), 47 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index cd7f6eca10..733d5fdc03 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -419,6 +419,8 @@ mod tests { use futures::StreamExt; use indoc::indoc; + use language::{Diagnostic, DiagnosticSet}; + use lsp::notification; use project::HoverBlock; use crate::test::EditorLspTestContext; @@ -426,7 +428,7 @@ mod tests { use super::*; #[gpui::test] - async fn test_hover_popover(cx: &mut gpui::TestAppContext) { + async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), @@ -460,19 +462,18 @@ mod tests { fn test() [println!]();"}); let mut requests = - cx.lsp - .handle_request::(move |_, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: indoc! {" + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: indoc! {" # Some basic docs Some test documentation"} - .to_string(), - }), - range: Some(symbol_range), - })) - }); + .to_string(), + }), + range: Some(symbol_range), + })) + }); cx.foreground() .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); requests.next().await; @@ -498,6 +499,9 @@ mod tests { let hover_point = cx.display_point(indoc! {" fn te|st() println!();"}); + let mut request = cx + .lsp + .handle_request::(|_, _| async move { Ok(None) }); cx.update_editor(|editor, cx| { hover_at( editor, @@ -507,15 +511,24 @@ mod tests { cx, ) }); - let mut request = cx - .lsp - .handle_request::(|_, _| async move { Ok(None) }); cx.foreground() .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); request.next().await; cx.editor(|editor, _| { assert!(!editor.hover_state.visible()); }); + } + + #[gpui::test] + async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; // Hover with keyboard has no delay cx.set_state(indoc! {" @@ -525,24 +538,23 @@ mod tests { let symbol_range = cx.lsp_range(indoc! {" [fn] test() println!();"}); - cx.lsp - .handle_request::(move |_, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: indoc! {" - # Some other basic docs - Some other test documentation"} - .to_string(), - }), - range: Some(symbol_range), - })) - }) - .next() - .await; - cx.foreground().run_until_parked(); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: indoc! {" + # Some other basic docs + Some other test documentation"} + .to_string(), + }), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; cx.editor(|editor, _| { - assert!(editor.hover_state.visible()); assert_eq!( editor.hover_state.info_popover.clone().unwrap().contents, vec![ @@ -558,4 +570,73 @@ mod tests { ) }); } + + #[gpui::test] + async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with just diagnostic, pops DiagnosticPopover immediately and then + // info popover once request completes + cx.set_state(indoc! {" + fn te|st() + println!();"}); + + // Send diagnostic to client + let range = cx.text_anchor_range(indoc! {" + fn [test]() + println!();"}); + cx.update_buffer(|buffer, cx| { + let snapshot = buffer.text_snapshot(); + let set = DiagnosticSet::from_sorted_entries( + vec![DiagnosticEntry { + range, + diagnostic: Diagnostic { + message: "A test diagnostic message.".to_string(), + ..Default::default() + }, + }], + &snapshot, + ); + buffer.update_diagnostics(set, cx); + }); + + // Hover pops diagnostic immediately + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + cx.condition(|Editor { hover_state, .. }, _| { + hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none() + }) + .await; + + // Info Popover shows after request responded to + let range = cx.lsp_range(indoc! {" + fn [test]() + println!();"}); + let mut requests = + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: indoc! {" + # Some other basic docs + Some other test documentation"} + .to_string(), + }), + range: Some(range), + })) + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + requests.next().await; + cx.condition(|Editor { hover_state, .. }, _| { + hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() + }) + .await; + } } diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index dd05a14bd6..1a57bcdb5c 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -9,9 +9,13 @@ use futures::{Future, StreamExt}; use indoc::indoc; use collections::BTreeMap; -use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle}; -use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection}; -use lsp::request; +use gpui::{ + json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle, +}; +use language::{ + point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig, Selection, +}; +use lsp::{notification, request}; use project::Project; use settings::Settings; use util::{ @@ -23,7 +27,8 @@ use workspace::{pane, AppState, Workspace, WorkspaceHandle}; use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, multi_buffer::ToPointUtf16, - AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint, + AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, EditorSnapshot, MultiBuffer, + ToPoint, }; #[cfg(test)] @@ -119,7 +124,7 @@ impl<'a> EditorTestContext<'a> { self.editor.condition(self.cx, predicate) } - pub fn editor(&mut self, read: F) -> T + pub fn editor(&self, read: F) -> T where F: FnOnce(&Editor, &AppContext) -> T, { @@ -133,12 +138,48 @@ impl<'a> EditorTestContext<'a> { self.editor.update(self.cx, update) } - pub fn buffer_text(&mut self) -> String { - self.editor.read_with(self.cx, |editor, cx| { - editor.buffer.read(cx).snapshot(cx).text() + pub fn multibuffer(&self, read: F) -> T + where + F: FnOnce(&MultiBuffer, &AppContext) -> T, + { + self.editor(|editor, cx| read(editor.buffer().read(cx), cx)) + } + + pub fn update_multibuffer(&mut self, update: F) -> T + where + F: FnOnce(&mut MultiBuffer, &mut ModelContext) -> T, + { + self.update_editor(|editor, cx| editor.buffer().update(cx, update)) + } + + pub fn buffer_text(&self) -> String { + self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) + } + + pub fn buffer(&self, read: F) -> T + where + F: FnOnce(&Buffer, &AppContext) -> T, + { + self.multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap().read(cx); + read(buffer, cx) }) } + pub fn update_buffer(&mut self, update: F) -> T + where + F: FnOnce(&mut Buffer, &mut ModelContext) -> T, + { + self.update_multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap(); + buffer.update(cx, update) + }) + } + + pub fn buffer_snapshot(&self) -> BufferSnapshot { + self.buffer(|buffer, _| buffer.snapshot()) + } + pub fn simulate_keystroke(&mut self, keystroke_text: &str) { let keystroke = Keystroke::parse(keystroke_text).unwrap(); let input = if keystroke.modified() { @@ -164,6 +205,18 @@ impl<'a> EditorTestContext<'a> { locations[0].to_display_point(&snapshot.display_snapshot) } + // Returns anchors for the current buffer using `[`..`]` + pub fn text_anchor_range(&self, marked_text: &str) -> Range { + let range_marker: TextRangeMarker = ('[', ']').into(); + let (unmarked_text, mut ranges) = + marked_text_ranges_by(&marked_text, vec![range_marker.clone()]); + assert_eq!(self.buffer_text(), unmarked_text); + let offset_range = ranges.remove(&range_marker).unwrap()[0].clone(); + let snapshot = self.buffer_snapshot(); + + snapshot.anchor_before(offset_range.start)..snapshot.anchor_after(offset_range.end) + } + // Sets the editor state via a marked string. // `|` characters represent empty selections // `[` to `}` represents a non empty selection with the head at `}` @@ -433,7 +486,7 @@ pub struct EditorLspTestContext<'a> { pub cx: EditorTestContext<'a>, pub lsp: lsp::FakeLanguageServer, pub workspace: ViewHandle, - pub editor_lsp_url: lsp::Url, + pub buffer_lsp_url: lsp::Url, } impl<'a> EditorLspTestContext<'a> { @@ -507,7 +560,7 @@ impl<'a> EditorLspTestContext<'a> { }, lsp, workspace, - editor_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), + buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), } } @@ -530,7 +583,7 @@ impl<'a> EditorLspTestContext<'a> { // Constructs lsp range using a marked string with '[', ']' range delimiters pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]); - assert_eq!(unmarked, self.cx.buffer_text()); + assert_eq!(unmarked, self.buffer_text()); let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone(); self.to_lsp_range(offset_range) } @@ -594,12 +647,16 @@ impl<'a> EditorLspTestContext<'a> { F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut, Fut: 'static + Send + Future>, { - let url = self.editor_lsp_url.clone(); + let url = self.buffer_lsp_url.clone(); self.lsp.handle_request::(move |params, cx| { let url = url.clone(); handler(url, params, cx) }) } + + pub fn notify(&self, params: T::Params) { + self.lsp.notify::(params); + } } impl<'a> Deref for EditorLspTestContext<'a> { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ee24539287..50c12d751e 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2462,11 +2462,11 @@ impl operation_queue::Operation for Operation { impl Default for Diagnostic { fn default() -> Self { Self { - code: Default::default(), + code: None, severity: DiagnosticSeverity::ERROR, message: Default::default(), - group_id: Default::default(), - is_primary: Default::default(), + group_id: 0, + is_primary: false, is_valid: true, is_disk_based: false, is_unnecessary: false, From b85b2a90aa5628ce4064f392ba4cd1c1de47871e Mon Sep 17 00:00:00 2001 From: K Simmons Date: Tue, 19 Jul 2022 16:09:09 -0700 Subject: [PATCH 22/29] address warnings --- crates/editor/src/hover_popover.rs | 1 - crates/editor/src/test.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 733d5fdc03..1509bad340 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -420,7 +420,6 @@ mod tests { use indoc::indoc; use language::{Diagnostic, DiagnosticSet}; - use lsp::notification; use project::HoverBlock; use crate::test::EditorLspTestContext; diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 1a57bcdb5c..9151f63e5b 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -27,8 +27,7 @@ use workspace::{pane, AppState, Workspace, WorkspaceHandle}; use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, multi_buffer::ToPointUtf16, - AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, EditorSnapshot, MultiBuffer, - ToPoint, + AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint, }; #[cfg(test)] From a076eeeb9690d319d2fafd6ff5b60aafa2e3fca3 Mon Sep 17 00:00:00 2001 From: K Simmons Date: Wed, 20 Jul 2022 13:43:57 -0700 Subject: [PATCH 23/29] tweak diagnostic popover colors --- styles/src/styleTree/hoverPopover.ts | 6 +++--- styles/src/themes/common/base16.ts | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/styles/src/styleTree/hoverPopover.ts b/styles/src/styleTree/hoverPopover.ts index aeac9c0166..0aac4e66c9 100644 --- a/styles/src/styleTree/hoverPopover.ts +++ b/styles/src/styleTree/hoverPopover.ts @@ -24,7 +24,7 @@ export default function HoverPopover(theme: Theme) { ...baseContainer, background: backgroundColor(theme, "on500Info"), border: { - color: theme.ramps.blue(0.2).hex(), + color: theme.ramps.blue(0).hex(), width: 1, }, }, @@ -32,7 +32,7 @@ export default function HoverPopover(theme: Theme) { ...baseContainer, background: backgroundColor(theme, "on500Warning"), border: { - color: theme.ramps.yellow(0.2).hex(), + color: theme.ramps.yellow(0).hex(), width: 1, }, }, @@ -40,7 +40,7 @@ export default function HoverPopover(theme: Theme) { ...baseContainer, background: backgroundColor(theme, "on500Error"), border: { - color: theme.ramps.red(0.2).hex(), + color: theme.ramps.red(0).hex(), width: 1, } }, diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index 78856d6191..321184d40d 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -89,9 +89,9 @@ export function createTheme( active: withOpacity(sample(ramps.red, 0.5), 0.25), }, on500Error: { - base: sample(ramps.red, 0.1), - hovered: sample(ramps.red, 0.15), - active: sample(ramps.red, 0.2), + base: sample(ramps.red, 0.05), + hovered: sample(ramps.red, 0.1), + active: sample(ramps.red, 0.15), }, warning: { base: withOpacity(sample(ramps.yellow, 0.5), 0.15), @@ -99,9 +99,9 @@ export function createTheme( active: withOpacity(sample(ramps.yellow, 0.5), 0.25), }, on500Warning: { - base: sample(ramps.yellow, 0.1), - hovered: sample(ramps.yellow, 0.15), - active: sample(ramps.yellow, 0.2), + base: sample(ramps.yellow, 0.05), + hovered: sample(ramps.yellow, 0.1), + active: sample(ramps.yellow, 0.15), }, info: { base: withOpacity(sample(ramps.blue, 0.5), 0.15), @@ -109,9 +109,9 @@ export function createTheme( active: withOpacity(sample(ramps.blue, 0.5), 0.25), }, on500Info: { - base: sample(ramps.blue, 0.1), - hovered: sample(ramps.blue, 0.15), - active: sample(ramps.blue, 0.2), + base: sample(ramps.blue, 0.05), + hovered: sample(ramps.blue, 0.1), + active: sample(ramps.blue, 0.15), }, }; From 7c0a03150623c19b6182d61a0d7772ec8e2e18cd Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 21 Jul 2022 10:04:12 -0700 Subject: [PATCH 24/29] added cursor text back --- crates/terminal/src/connection.rs | 6 ++++-- crates/terminal/src/terminal_element.rs | 13 ++++++------- crates/terminal/src/tests/terminal_test_context.rs | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index 48160cd877..f7407959f6 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -366,7 +366,7 @@ impl Terminal { pub fn render_lock(&self, new_size: Option, f: F) -> T where - F: FnOnce(RenderableContent) -> T, + F: FnOnce(RenderableContent, char) -> T, { if let Some(new_size) = new_size { self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size @@ -380,7 +380,9 @@ impl Terminal { } let content = term.renderable_content(); - f(content) + let cursor_text = term.grid()[content.cursor.point].c; + + f(content, cursor_text) } pub fn get_display_offset(&self) -> usize { diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 3383359f5c..ea547a404f 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -333,7 +333,7 @@ impl Element for TerminalEl { let terminal = self.connection.upgrade(cx).unwrap().read(cx); let (cursor, cells, rects, highlights) = - terminal.render_lock(Some(layout.size.clone()), |content| { + terminal.render_lock(Some(layout.size.clone()), |content, cursor_text| { let (cells, rects, highlights) = layout_grid( content.display_iter, &layout.text_style, @@ -345,7 +345,7 @@ impl Element for TerminalEl { //Layout cursor let cursor = layout_cursor( - // grid, + cursor_text, cx.text_layout_cache, &layout, content.cursor.point, @@ -522,14 +522,14 @@ impl Element for TerminalEl { ///TODO: Fix cursor rendering with alacritty fork fn layout_cursor( - // grid: &Grid, + cursor_text: char, text_layout_cache: &TextLayoutCache, tcx: &TerminalLayoutData, cursor_point: Point, display_offset: usize, constraint: SizeConstraint, ) -> Option { - let cursor_text = layout_cursor_text(/*grid,*/ cursor_point, text_layout_cache, tcx); + let cursor_text = layout_cursor_text(cursor_text, cursor_point, text_layout_cache, tcx); get_cursor_shape( cursor_point.line.0 as usize, cursor_point.column.0 as usize, @@ -558,13 +558,12 @@ fn layout_cursor( } fn layout_cursor_text( - // grid: &Grid, + cursor_text: char, _cursor_point: Point, text_layout_cache: &TextLayoutCache, tcx: &TerminalLayoutData, ) -> Line { - let cursor_text = " "; //grid[cursor_point.line][cursor_point.column].c.to_string(); - + let cursor_text = cursor_text.to_string(); text_layout_cache.layout_str( &cursor_text, tcx.text_style.font_size, diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index 429d397d1f..f4678045d4 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -57,7 +57,7 @@ impl<'a> TerminalTestContext<'a> { } fn grid_as_str(connection: &Terminal) -> String { - connection.render_lock(None, |content| { + connection.render_lock(None, |content, _| { let lines = content.display_iter.group_by(|i| i.point.line.0); lines .into_iter() From c6d5decbf956fd1f1b3f64868af3334515eec8ef Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 21 Jul 2022 12:37:15 -0700 Subject: [PATCH 25/29] Finished erorr terminal refactoring --- crates/command_palette/src/command_palette.rs | 14 +- crates/file_finder/src/file_finder.rs | 10 +- crates/terminal/src/connection.rs | 12 +- crates/terminal/src/modal.rs | 79 ++- crates/terminal/src/terminal.rs | 455 ++++++++++-------- crates/terminal/src/terminal_element.rs | 24 +- .../src/tests/terminal_test_context.rs | 6 +- crates/workspace/src/workspace.rs | 6 +- 8 files changed, 311 insertions(+), 295 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 467a45b93e..5f438057ee 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -362,12 +362,7 @@ mod tests { }); let palette = workspace.read_with(cx, |workspace, _| { - workspace - .modal() - .unwrap() - .clone() - .downcast::() - .unwrap() + workspace.modal::().unwrap() }); palette @@ -398,12 +393,7 @@ mod tests { // Assert editor command not present let palette = workspace.read_with(cx, |workspace, _| { - workspace - .modal() - .unwrap() - .clone() - .downcast::() - .unwrap() + workspace.modal::().unwrap() }); palette diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 720e0142be..c8172e0de3 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -317,15 +317,7 @@ mod tests { let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx)); cx.dispatch_action(window_id, Toggle); - let finder = cx.read(|cx| { - workspace - .read(cx) - .modal() - .cloned() - .unwrap() - .downcast::() - .unwrap() - }); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); cx.dispatch_action(window_id, Input("b".into())); cx.dispatch_action(window_id, Input("n".into())); cx.dispatch_action(window_id, Input("a".into())); diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/connection.rs index f7407959f6..d41f4e8fad 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/connection.rs @@ -122,18 +122,18 @@ impl Display for TerminalError { } } -pub struct DisconnectedPTY { +pub struct TerminalBuilder { terminal: Terminal, events_rx: UnboundedReceiver, } -impl DisconnectedPTY { +impl TerminalBuilder { pub fn new( working_directory: Option, shell: Option, env: Option>, initial_size: TerminalDimensions, - ) -> Result { + ) -> Result { let pty_config = { let alac_shell = shell.clone().and_then(|shell| match shell { Shell::System => None, @@ -214,13 +214,13 @@ impl DisconnectedPTY { associated_directory: working_directory, }; - Ok(DisconnectedPTY { + Ok(TerminalBuilder { terminal, events_rx, }) } - pub fn connect(mut self, cx: &mut ModelContext) -> Terminal { + pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { cx.spawn_weak(|this, mut cx| async move { //Listen for terminal events while let Some(event) = self.events_rx.next().await { @@ -435,7 +435,7 @@ impl Entity for Terminal { mod alacritty_unix { use alacritty_terminal::config::Program; use gpui::anyhow::{bail, Result}; - use libc::{self}; + use libc; use std::ffi::CStr; use std::mem::MaybeUninit; use std::ptr; diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 0cd0febe78..c53f2a0469 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -1,60 +1,60 @@ use gpui::{ModelHandle, ViewContext}; use workspace::Workspace; -use crate::{connection::Terminal, get_wd_for_workspace, DeployModal, Event, TerminalView}; +use crate::{ + connection::Terminal, get_working_directory, DeployModal, Event, TerminalContent, TerminalView, +}; #[derive(Debug)] -struct StoredConnection(ModelHandle); +struct StoredTerminal(ModelHandle); pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext) { // Pull the terminal connection out of the global if it has been stored - let possible_connection = - cx.update_default_global::, _, _>(|possible_connection, _| { + let possible_terminal = + cx.update_default_global::, _, _>(|possible_connection, _| { possible_connection.take() }); - if let Some(StoredConnection(stored_connection)) = possible_connection { - // Create a view from the stored connection + if let Some(StoredTerminal(stored_terminal)) = possible_terminal { workspace.toggle_modal(cx, |_, cx| { - cx.add_view(|cx| { - TerminalView::from_connection( - crate::TerminalConnection(Ok(stored_connection.clone())), - true, - cx, - ) - }) + // Create a view from the stored connection if the terminal modal is not already shown + cx.add_view(|cx| TerminalView::from_terminal(stored_terminal.clone(), true, cx)) }); - cx.set_global::>(Some(StoredConnection( - stored_connection.clone(), - ))); + // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must + // store the terminal back in the global + cx.set_global::>(Some(StoredTerminal(stored_terminal.clone()))); } else { // No connection was stored, create a new terminal if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { - let wd = get_wd_for_workspace(workspace, cx); + // No terminal modal visible, construct a new one. + let working_directory = get_working_directory(workspace, cx); - //TODO fix this crash - let this = cx.add_view(|cx| TerminalView::new(wd, true, cx).unwrap()); + let this = cx.add_view(|cx| TerminalView::new(working_directory, true, cx)); + + if let TerminalContent::Connected(connected) = &this.read(cx).content { + let terminal_handle = connected.read(cx).terminal.clone(); + cx.subscribe(&terminal_handle, on_event).detach(); + // Set the global immediately if terminal construction was successful, + // in case the user opens the command palette + cx.set_global::>(Some(StoredTerminal( + terminal_handle.clone(), + ))); + } - let connection_handle = this.read(cx).connection.0.as_ref().unwrap().clone(); - cx.subscribe(&connection_handle, on_event).detach(); - //Set the global immediately, in case the user opens the command palette - cx.set_global::>(Some(StoredConnection( - connection_handle.clone(), - ))); this }) { - let connection = closed_terminal_handle - .read(cx) - .connection - .0 - .as_ref() - .unwrap() - .clone(); - cx.set_global(Some(StoredConnection(connection))); + // Terminal modal was dismissed. Store terminal if the terminal view is connected + if let TerminalContent::Connected(connected) = &closed_terminal_handle.read(cx).content + { + let terminal_handle = connected.read(cx).terminal.clone(); + // Set the global immediately if terminal construction was successful, + // in case the user opens the command palette + cx.set_global::>(Some(StoredTerminal( + terminal_handle.clone(), + ))); + } } } - - //The problem is that the terminal modal is never re-stored. } pub fn on_event( @@ -65,13 +65,8 @@ pub fn on_event( ) { // Dismiss the modal if the terminal quit if let Event::CloseTerminal = event { - cx.set_global::>(None); - if workspace - .modal() - .cloned() - .and_then(|modal| modal.downcast::()) - .is_some() - { + cx.set_global::>(None); + if workspace.modal::().is_some() { workspace.dismiss_modal(cx) } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0bbad588c5..9850ff716b 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -3,11 +3,11 @@ pub mod connection; mod modal; pub mod terminal_element; -use connection::{DisconnectedPTY, Event, Terminal, TerminalError}; +use connection::{Event, Terminal, TerminalBuilder, TerminalError}; use dirs::home_dir; use gpui::{ - actions, elements::*, geometry::vector::vec2f, keymap::Keystroke, AppContext, ClipboardItem, - Entity, ModelHandle, MutableAppContext, View, ViewContext, + actions, elements::*, geometry::vector::vec2f, keymap::Keystroke, AnyViewHandle, AppContext, + ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, }; use modal::deploy_modal; @@ -16,7 +16,6 @@ use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::path::{Path, PathBuf}; use terminal_element::{terminal_layout_context::TerminalLayoutData, TerminalDimensions}; -use util::ResultExt; use workspace::{Item, Workspace}; use crate::terminal_element::TerminalEl; @@ -49,25 +48,49 @@ actions!( ///Initialize and register all of our action handlers 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); + cx.add_action(ConnectedView::ctrl_c); + cx.add_action(ConnectedView::up); + cx.add_action(ConnectedView::down); + cx.add_action(ConnectedView::escape); + cx.add_action(ConnectedView::enter); //Useful terminal actions - cx.add_action(TerminalView::deploy); + cx.add_action(ConnectedView::deploy); + cx.add_action(ConnectedView::copy); + cx.add_action(ConnectedView::paste); + cx.add_action(ConnectedView::clear); cx.add_action(deploy_modal); - cx.add_action(TerminalView::copy); - cx.add_action(TerminalView::paste); - cx.add_action(TerminalView::clear); } -//New Type to make terminal connection's easier -struct TerminalConnection(Result, TerminalError>); +//Make terminal view an enum, that can give you views for the error and non-error states +//Take away all the result unwrapping in the current TerminalView by making it 'infallible' +//Bubble up to deploy(_modal)() calls + +enum TerminalContent { + Connected(ViewHandle), + Error(ViewHandle), +} + +impl TerminalContent { + fn handle(&self) -> AnyViewHandle { + match self { + Self::Connected(handle) => handle.into(), + Self::Error(handle) => handle.into(), + } + } +} + +pub struct TerminalView { + modal: bool, + content: TerminalContent, +} + +pub struct ErrorView { + error: TerminalError, +} ///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct TerminalView { - connection: TerminalConnection, +pub struct ConnectedView { + terminal: ModelHandle, has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, @@ -79,14 +102,18 @@ impl Entity for TerminalView { type Event = Event; } +impl Entity for ConnectedView { + type Event = Event; +} + +impl Entity for ErrorView { + type Event = Event; +} + impl TerminalView { ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` - fn new( - working_directory: Option, - modal: bool, - cx: &mut ViewContext, - ) -> Option { + fn new(working_directory: Option, modal: bool, cx: &mut ViewContext) -> Self { //The details here don't matter, the terminal will be resized on the first layout let size_info = TerminalDimensions::new( DEBUG_LINE_HEIGHT, @@ -98,156 +125,37 @@ impl TerminalView { let shell = settings.terminal_overrides.shell.clone(); let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. - let connection = DisconnectedPTY::new(working_directory, shell, envs, size_info) - .map(|pty| cx.add_model(|cx| pty.connect(cx))) - .map_err(|err| { - match err.downcast::() { - Ok(err) => err, - Err(_) => unreachable!(), //This should never happen - } - }); + let content = match TerminalBuilder::new(working_directory, shell, envs, size_info) { + Ok(terminal) => { + let terminal = cx.add_model(|cx| terminal.subscribe(cx)); + let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); + cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone())) + .detach(); + TerminalContent::Connected(view) + } + Err(error) => { + let view = cx.add_view(|_| ErrorView { + error: error.downcast::().unwrap(), + }); + TerminalContent::Error(view) + } + }; + cx.focus(content.handle()); - if let Ok(_) = connection { - Some(TerminalView::from_connection( - TerminalConnection(connection), - modal, - cx, - )) - } else { - connection.log_err(); - None - } + TerminalView { modal, content } } - fn from_connection( - connection: TerminalConnection, + fn from_terminal( + terminal: ModelHandle, modal: bool, cx: &mut ViewContext, - ) -> TerminalView { - match connection.0.as_ref() { - Ok(conn) => { - cx.observe(conn, |_, _, cx| cx.notify()).detach(); - cx.subscribe(conn, |this, _, event, cx| match event { - Event::Wakeup => { - if cx.is_self_focused() { - cx.notify() - } else { - this.has_new_content = true; - cx.emit(Event::TitleChanged); - } - } - Event::Bell => { - this.has_bell = true; - cx.emit(Event::TitleChanged); - } - _ => cx.emit(*event), - }) - .detach(); - } - Err(_) => { /* Leave it as is */ } - } - + ) -> Self { + let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); TerminalView { - connection, - has_new_content: true, - has_bell: false, modal, + content: TerminalContent::Connected(connected_view), } } - - fn clear_bel(&mut self, cx: &mut ViewContext) { - self.has_bell = false; - cx.emit(Event::TitleChanged); - } - - ///Create a new Terminal in the current working directory or the user's home directory - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - let wd = get_wd_for_workspace(workspace, cx); - if let Some(view) = cx.add_option_view(|cx| TerminalView::new(wd, false, cx)) { - workspace.add_item(Box::new(view), cx); - } - } - - fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { - self.connection - .0 - .as_ref() - .map(|term_handle| term_handle.read(cx).clear()) - .ok(); - } - - ///Attempt to paste the clipboard into the terminal - fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - self.connection - .0 - .as_ref() - .map(|handle| handle.read(cx)) - .map(|term| term.copy()) - .map(|text| text.map(|text| cx.write_to_clipboard(ClipboardItem::new(text)))) - .ok(); - } - - ///Attempt to paste the clipboard into the terminal - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - cx.read_from_clipboard().map(|item| { - self.connection - .0 - .as_ref() - .map(|handle| handle.read(cx)) - .map(|term| term.paste(item.text())) - .ok(); - }); - } - - ///Synthesize the keyboard event corresponding to 'up' - fn up(&mut self, _: &Up, cx: &mut ViewContext) { - self.connection - .0 - .as_ref() - .map(|handle| handle.read(cx)) - .map(|term| term.try_keystroke(&Keystroke::parse("up").unwrap())) - .ok(); - } - - ///Synthesize the keyboard event corresponding to 'down' - fn down(&mut self, _: &Down, cx: &mut ViewContext) { - self.connection - .0 - .as_ref() - .map(|handle| handle.read(cx)) - .map(|term| term.try_keystroke(&Keystroke::parse("down").unwrap())) - .ok(); - } - - ///Synthesize the keyboard event corresponding to 'ctrl-c' - fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { - self.connection - .0 - .as_ref() - .map(|handle| handle.read(cx)) - .map(|term| term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap())) - .ok(); - } - - ///Synthesize the keyboard event corresponding to 'escape' - fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { - self.connection - .0 - .as_ref() - .map(|handle| handle.read(cx)) - .map(|term| term.try_keystroke(&Keystroke::parse("escape").unwrap())) - .ok(); - } - - ///Synthesize the keyboard event corresponding to 'enter' - fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { - self.connection - .0 - .as_ref() - .map(|handle| handle.read(cx)) - .map(|term| term.try_keystroke(&Keystroke::parse("enter").unwrap())) - .ok(); - } } impl View for TerminalView { @@ -256,48 +164,25 @@ impl View for TerminalView { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let element = match self.connection.0.as_ref() { - Ok(handle) => { - let connection_handle = handle.clone().downgrade(); - TerminalEl::new(cx.handle(), connection_handle, self.modal).contained() - } - Err(e) => { - let settings = cx.global::(); - let style = TerminalLayoutData::make_text_style(cx.font_cache(), settings); - - Flex::column() - .with_child( - Flex::row() - .with_child( - Label::new( - format!( - "Failed to open the terminal. Info: \n{}", - e.to_string() - ), - style, - ) - .boxed(), - ) - .aligned() - .boxed(), - ) - .aligned() - .contained() - } + let child_view = match &self.content { + TerminalContent::Connected(connected) => ChildView::new(connected), + TerminalContent::Error(error) => ChildView::new(error), }; if self.modal { let settings = cx.global::(); let container_style = settings.theme.terminal.modal_container; - element.with_style(container_style).boxed() + child_view.contained().with_style(container_style).boxed() } else { - element.boxed() + child_view.boxed() } } fn on_focus(&mut self, cx: &mut ViewContext) { cx.emit(Event::Activate); - self.has_new_content = false; + cx.defer(|view, cx| { + cx.focus(view.content.handle()); + }); } fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { @@ -309,6 +194,144 @@ impl View for TerminalView { } } +impl ConnectedView { + fn from_terminal( + terminal: ModelHandle, + modal: bool, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); + cx.subscribe(&terminal, |this, _, event, cx| match event { + Event::Wakeup => { + if cx.is_self_focused() { + cx.notify() + } else { + this.has_new_content = true; + cx.emit(Event::TitleChanged); + } + } + Event::Bell => { + this.has_bell = true; + cx.emit(Event::TitleChanged); + } + _ => cx.emit(*event), + }) + .detach(); + + Self { + terminal, + has_new_content: true, + has_bell: false, + modal, + } + } + + fn clear_bel(&mut self, cx: &mut ViewContext) { + self.has_bell = false; + cx.emit(Event::TitleChanged); + } + + ///Create a new Terminal in the current working directory or the user's home directory + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + let working_directory = get_working_directory(workspace, cx); + let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); + workspace.add_item(Box::new(view), cx); + } + + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { + self.terminal.read(cx).clear(); + } + + ///Attempt to paste the clipboard into the terminal + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + self.terminal + .read(cx) + .copy() + .map(|text| cx.write_to_clipboard(ClipboardItem::new(text))); + } + + ///Attempt to paste the clipboard into the terminal + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + cx.read_from_clipboard().map(|item| { + self.terminal.read(cx).paste(item.text()); + }); + } + + ///Synthesize the keyboard event corresponding to 'up' + fn up(&mut self, _: &Up, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("up").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'down' + fn down(&mut self, _: &Down, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("down").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'ctrl-c' + fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'escape' + fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("escape").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'enter' + fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("enter").unwrap()); + } +} + +impl View for ConnectedView { + fn ui_name() -> &'static str { + "Connected Terminal View" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let terminal_handle = self.terminal.clone().downgrade(); + TerminalEl::new(cx.handle(), terminal_handle, self.modal) + .contained() + .boxed() + } + + fn on_focus(&mut self, _cx: &mut ViewContext) { + self.has_new_content = false; + } +} + +impl View for ErrorView { + fn ui_name() -> &'static str { + "Terminal Error" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let settings = cx.global::(); + let style = TerminalLayoutData::make_text_style(cx.font_cache(), settings); + + Label::new( + format!( + "Failed to open the terminal. Info: \n{}", + self.error.to_string() + ), + style, + ) + .aligned() + .contained() + .boxed() + } +} + impl Item for TerminalView { fn tab_content( &self, @@ -316,9 +339,11 @@ impl Item for TerminalView { tab_theme: &theme::Tab, cx: &gpui::AppContext, ) -> ElementBox { - let title = match self.connection.0.as_ref() { - Ok(handle) => handle.read(cx).title.clone(), - Err(_) => "Terminal".to_string(), + let title = match &self.content { + TerminalContent::Connected(connected) => { + connected.read(cx).terminal.read(cx).title.clone() + } + TerminalContent::Error(_) => "Terminal".to_string(), }; Flex::row() @@ -335,13 +360,17 @@ impl Item for TerminalView { //From what I can tell, there's no way to tell the current working //Directory of the terminal from outside the shell. There might be //solutions to this, but they are non-trivial and require more IPC - - let wd = match self.connection.0.as_ref() { - Ok(term_handle) => term_handle.read(cx).associated_directory.clone(), - Err(e) => e.directory.clone(), - }; - - TerminalView::new(wd, false, cx) + if let TerminalContent::Connected(connected) = &self.content { + let associated_directory = connected + .read(cx) + .terminal + .read(cx) + .associated_directory + .clone(); + Some(TerminalView::new(associated_directory, false, cx)) + } else { + None + } } fn project_path(&self, _cx: &gpui::AppContext) -> Option { @@ -387,12 +416,20 @@ impl Item for TerminalView { gpui::Task::ready(Ok(())) } - fn is_dirty(&self, _: &gpui::AppContext) -> bool { - self.has_new_content + fn is_dirty(&self, cx: &gpui::AppContext) -> bool { + if let TerminalContent::Connected(connected) = &self.content { + connected.read(cx).has_new_content + } else { + false + } } - fn has_conflict(&self, _: &AppContext) -> bool { - self.has_bell + fn has_conflict(&self, cx: &AppContext) -> bool { + if let TerminalContent::Connected(connected) = &self.content { + connected.read(cx).has_bell + } else { + false + } } fn should_update_tab_on_event(event: &Self::Event) -> bool { @@ -409,7 +446,7 @@ impl Item for TerminalView { } ///Get's the working directory for the given workspace, respecting the user's settings. -fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option { +fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option { let wd_setting = cx .global::() .terminal_overrides diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index ea547a404f..89618a9f3d 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -32,7 +32,7 @@ use util::ResultExt; use std::{cmp::min, ops::Range}; use std::{fmt::Debug, ops::Sub}; -use crate::{color_translation::convert_color, connection::Terminal, TerminalView}; +use crate::{color_translation::convert_color, connection::Terminal, ConnectedView}; use self::terminal_layout_context::TerminalLayoutData; @@ -44,8 +44,8 @@ const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; ///The GPUI element that paints the terminal. ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalEl { - connection: WeakModelHandle, - view: WeakViewHandle, + terminal: WeakModelHandle, + view: WeakViewHandle, modal: bool, } @@ -227,13 +227,13 @@ pub struct LayoutState { impl TerminalEl { pub fn new( - view: WeakViewHandle, - connection: WeakModelHandle, + view: WeakViewHandle, + terminal: WeakModelHandle, modal: bool, ) -> TerminalEl { TerminalEl { view, - connection, + terminal, modal, } } @@ -246,9 +246,9 @@ impl TerminalEl { cur_size: TerminalDimensions, cx: &mut PaintContext, ) { - let mouse_down_connection = self.connection.clone(); - let click_connection = self.connection.clone(); - let drag_connection = self.connection.clone(); + let mouse_down_connection = self.terminal.clone(); + let click_connection = self.terminal.clone(); + let drag_connection = self.terminal.clone(); cx.scene.push_mouse_region( MouseRegion::new(view_id, None, visible_bounds) .on_down( @@ -330,7 +330,7 @@ impl Element for TerminalEl { let layout = TerminalLayoutData::new(cx.global::(), &cx.font_cache(), constraint.max); - let terminal = self.connection.upgrade(cx).unwrap().read(cx); + let terminal = self.terminal.upgrade(cx).unwrap().read(cx); let (cursor, cells, rects, highlights) = terminal.render_lock(Some(layout.size.clone()), |content, cursor_text| { @@ -476,7 +476,7 @@ impl Element for TerminalEl { let vertical_scroll = (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER; - self.connection.upgrade(cx.app).map(|terminal| { + self.terminal.upgrade(cx.app).map(|terminal| { terminal .read(cx.app) .scroll(Scroll::Delta(vertical_scroll.round() as i32)); @@ -493,7 +493,7 @@ impl Element for TerminalEl { view.update(cx.app, |view, cx| view.clear_bel(cx)) } - self.connection + self.terminal .upgrade(cx.app) .map(|model_handle| model_handle.read(cx.app)) .map(|term| term.try_keystroke(keystroke)) diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index f4678045d4..4066882acf 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -4,7 +4,7 @@ use gpui::{geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, Test use itertools::Itertools; use crate::{ - connection::{DisconnectedPTY, Terminal}, + connection::{Terminal, TerminalBuilder}, terminal_element::TerminalDimensions, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH, }; @@ -25,9 +25,9 @@ impl<'a> TerminalTestContext<'a> { ); let connection = cx.add_model(|cx| { - DisconnectedPTY::new(None, None, None, size_info) + TerminalBuilder::new(None, None, None, size_info) .unwrap() - .connect(cx) + .subscribe(cx) }); TerminalTestContext { cx, connection } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8462967da5..9bda057e39 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1223,8 +1223,10 @@ impl Workspace { } } - pub fn modal(&self) -> Option<&AnyViewHandle> { - self.modal.as_ref() + pub fn modal(&self) -> Option> { + self.modal + .as_ref() + .and_then(|modal| modal.clone().downcast::()) } pub fn dismiss_modal(&mut self, cx: &mut ViewContext) { From dce27870ce7e0ede39548f0a832fa52235043cec Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 22 Jul 2022 10:20:15 -0700 Subject: [PATCH 26/29] Refactored terminal tests --- crates/terminal/src/terminal.rs | 158 +++--------------- .../src/tests/terminal_test_context.rs | 108 ++++++++++-- 2 files changed, 120 insertions(+), 146 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9850ff716b..ab543a22dc 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -507,7 +507,6 @@ mod tests { use gpui::TestAppContext; use std::path::Path; - use workspace::AppState; mod terminal_test_context; @@ -515,7 +514,7 @@ mod tests { //and produce noticable output? #[gpui::test(retries = 5)] async fn test_terminal(cx: &mut TestAppContext) { - let mut cx = TerminalTestContext::new(cx); + let mut cx = TerminalTestContext::new(cx, true); cx.execute_and_wait("expr 3 + 4", |content, _cx| content.contains("7")) .await; @@ -527,12 +526,10 @@ mod tests { #[gpui::test] async fn no_worktree(cx: &mut TestAppContext) { //Setup variables - let params = cx.update(AppState::test); - let project = Project::test(params.fs.clone(), [], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx)); - + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; //Test - cx.read(|cx| { + cx.cx.read(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -551,28 +548,12 @@ mod tests { #[gpui::test] async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { //Setup variables - let params = cx.update(AppState::test); - let project = Project::test(params.fs.clone(), [], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx)); - let (wt, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root.txt", true, cx) - }) - .await - .unwrap(); - cx.update(|cx| { - wt.update(cx, |wt, cx| { - wt.as_local() - .unwrap() - .create_entry(Path::new(""), false, cx) - }) - }) - .await - .unwrap(); + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + cx.create_file_wt(project.clone(), "/root.txt").await; - //Test - cx.read(|cx| { + cx.cx.read(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -591,27 +572,12 @@ mod tests { #[gpui::test] async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { //Setup variables - let params = cx.update(AppState::test); - let project = Project::test(params.fs.clone(), [], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx)); - let (wt, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root/", true, cx) - }) - .await - .unwrap(); - - //Setup root folder - cx.update(|cx| { - wt.update(cx, |wt, cx| { - wt.as_local().unwrap().create_entry(Path::new(""), true, cx) - }) - }) - .await - .unwrap(); + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await; //Test - cx.update(|cx| { + cx.cx.update(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -629,53 +595,14 @@ mod tests { #[gpui::test] async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { //Setup variables - let params = cx.update(AppState::test); - let project = Project::test(params.fs.clone(), [], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx)); - let (wt1, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root1/", true, cx) - }) - .await - .unwrap(); - - let (wt2, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root2.txt", true, cx) - }) - .await - .unwrap(); - - //Setup root - let _ = cx - .update(|cx| { - wt1.update(cx, |wt, cx| { - wt.as_local().unwrap().create_entry(Path::new(""), true, cx) - }) - }) - .await - .unwrap(); - let entry2 = cx - .update(|cx| { - wt2.update(cx, |wt, cx| { - wt.as_local() - .unwrap() - .create_entry(Path::new(""), false, cx) - }) - }) - .await - .unwrap(); - - cx.update(|cx| { - let p = ProjectPath { - worktree_id: wt2.read(cx).id(), - path: entry2.path, - }; - project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); - }); + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; + let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await; + cx.insert_active_entry_for(wt2, entry2, project.clone()); //Test - cx.update(|cx| { + cx.cx.update(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -692,51 +619,14 @@ mod tests { #[gpui::test] async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { //Setup variables - let params = cx.update(AppState::test); - let project = Project::test(params.fs.clone(), [], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx)); - let (wt1, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root1/", true, cx) - }) - .await - .unwrap(); - - let (wt2, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root2/", true, cx) - }) - .await - .unwrap(); - - //Setup root - let _ = cx - .update(|cx| { - wt1.update(cx, |wt, cx| { - wt.as_local().unwrap().create_entry(Path::new(""), true, cx) - }) - }) - .await - .unwrap(); - let entry2 = cx - .update(|cx| { - wt2.update(cx, |wt, cx| { - wt.as_local().unwrap().create_entry(Path::new(""), true, cx) - }) - }) - .await - .unwrap(); - - cx.update(|cx| { - let p = ProjectPath { - worktree_id: wt2.read(cx).id(), - path: entry2.path, - }; - project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); - }); + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; + let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await; + cx.insert_active_entry_for(wt2, entry2, project.clone()); //Test - cx.update(|cx| { + cx.cx.update(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index 4066882acf..f9b99d60d8 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -1,7 +1,11 @@ -use std::time::Duration; +use std::{path::Path, time::Duration}; -use gpui::{geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, TestAppContext}; +use gpui::{ + geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, TestAppContext, ViewHandle, +}; use itertools::Itertools; +use project::{Entry, Project, ProjectPath, Worktree}; +use workspace::{AppState, Workspace}; use crate::{ connection::{Terminal, TerminalBuilder}, @@ -11,11 +15,11 @@ use crate::{ pub struct TerminalTestContext<'a> { pub cx: &'a mut TestAppContext, - pub connection: ModelHandle, + pub connection: Option>, } impl<'a> TerminalTestContext<'a> { - pub fn new(cx: &'a mut TestAppContext) -> Self { + pub fn new(cx: &'a mut TestAppContext, term: bool) -> Self { cx.set_condition_duration(Some(Duration::from_secs(5))); let size_info = TerminalDimensions::new( @@ -24,10 +28,12 @@ impl<'a> TerminalTestContext<'a> { vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), ); - let connection = cx.add_model(|cx| { - TerminalBuilder::new(None, None, None, size_info) - .unwrap() - .subscribe(cx) + let connection = term.then(|| { + cx.add_model(|cx| { + TerminalBuilder::new(None, None, None, size_info) + .unwrap() + .subscribe(cx) + }) }); TerminalTestContext { cx, connection } @@ -37,23 +43,101 @@ impl<'a> TerminalTestContext<'a> { where F: Fn(String, &AppContext) -> bool, { + let connection = self.connection.take().unwrap(); + let command = command.to_string(); - self.connection.update(self.cx, |connection, _| { + connection.update(self.cx, |connection, _| { connection.write_to_pty(command); connection.write_to_pty("\r".to_string()); }); - self.connection + connection .condition(self.cx, |conn, cx| { let content = Self::grid_as_str(conn); f(content, cx) }) .await; - self.cx - .read_model_with(&self.connection, &mut |conn, _: &AppContext| { + let res = self + .cx + .read_model_with(&connection, &mut |conn, _: &AppContext| { Self::grid_as_str(conn) + }); + + self.connection = Some(connection); + + res + } + + ///Creates a worktree with 1 file: /root.txt + pub async fn blank_workspace(&mut self) -> (ModelHandle, ViewHandle) { + let params = self.cx.update(AppState::test); + + let project = Project::test(params.fs.clone(), [], self.cx).await; + let (_, workspace) = self.cx.add_window(|cx| Workspace::new(project.clone(), cx)); + + (project, workspace) + } + + ///Creates a worktree with 1 folder: /root{suffix}/ + pub async fn create_folder_wt( + &mut self, + project: ModelHandle, + path: impl AsRef, + ) -> (ModelHandle, Entry) { + self.create_wt(project, true, path).await + } + + ///Creates a worktree with 1 file: /root{suffix}.txt + pub async fn create_file_wt( + &mut self, + project: ModelHandle, + path: impl AsRef, + ) -> (ModelHandle, Entry) { + self.create_wt(project, false, path).await + } + + async fn create_wt( + &mut self, + project: ModelHandle, + is_dir: bool, + path: impl AsRef, + ) -> (ModelHandle, Entry) { + let (wt, _) = project + .update(self.cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) }) + .await + .unwrap(); + + let entry = self + .cx + .update(|cx| { + wt.update(cx, |wt, cx| { + wt.as_local() + .unwrap() + .create_entry(Path::new(""), is_dir, cx) + }) + }) + .await + .unwrap(); + + (wt, entry) + } + + pub fn insert_active_entry_for( + &mut self, + wt: ModelHandle, + entry: Entry, + project: ModelHandle, + ) { + self.cx.update(|cx| { + let p = ProjectPath { + worktree_id: wt.read(cx).id(), + path: entry.path, + }; + project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); + }); } fn grid_as_str(connection: &Terminal) -> String { From b93040a6aea4f005f83cc819ac06b10bfc1df4eb Mon Sep 17 00:00:00 2001 From: K Simmons Date: Fri, 22 Jul 2022 10:55:57 -0700 Subject: [PATCH 27/29] attempt to fix broken test --- crates/editor/src/hover_popover.rs | 42 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 1509bad340..99669cb15d 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -608,34 +608,34 @@ mod tests { // Hover pops diagnostic immediately cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); - cx.condition(|Editor { hover_state, .. }, _| { - hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none() - }) - .await; + cx.foreground().run_until_parked(); + + cx.editor(|Editor { hover_state, .. }, _| { + assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) + }); // Info Popover shows after request responded to let range = cx.lsp_range(indoc! {" fn [test]() println!();"}); - let mut requests = - cx.handle_request::(move |_, _, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: indoc! {" - # Some other basic docs - Some other test documentation"} - .to_string(), - }), - range: Some(range), - })) - }); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: indoc! {" + # Some other basic docs + Some other test documentation"} + .to_string(), + }), + range: Some(range), + })) + }); cx.foreground() .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - requests.next().await; - cx.condition(|Editor { hover_state, .. }, _| { + + cx.foreground().run_until_parked(); + cx.editor(|Editor { hover_state, .. }, _| { hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() - }) - .await; + }); } } From 119207a9e55c433ce1190b46b8f8eb1d15c811a9 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 22 Jul 2022 12:06:42 -0700 Subject: [PATCH 28/29] Refactored a bunch of stuff, working on tidying element code --- .../{terminal_element.rs => connected_el.rs} | 670 +++++++++--------- crates/terminal/src/connected_view.rs | 162 +++++ .../colors.rs} | 2 +- .../keymappings.rs => mappings/keys.rs} | 9 - crates/terminal/src/mappings/mod.rs | 2 + .../terminal/src/{modal.rs => modal_view.rs} | 6 +- .../terminal/src/{connection.rs => model.rs} | 15 +- crates/terminal/src/terminal.rs | 201 +----- .../terminal_layout_context.rs | 60 -- .../src/tests/terminal_test_context.rs | 6 +- 10 files changed, 549 insertions(+), 584 deletions(-) rename crates/terminal/src/{terminal_element.rs => connected_el.rs} (57%) create mode 100644 crates/terminal/src/connected_view.rs rename crates/terminal/src/{color_translation.rs => mappings/colors.rs} (98%) rename crates/terminal/src/{connection/keymappings.rs => mappings/keys.rs} (99%) create mode 100644 crates/terminal/src/mappings/mod.rs rename crates/terminal/src/{modal.rs => modal_view.rs} (91%) rename crates/terminal/src/{connection.rs => model.rs} (98%) delete mode 100644 crates/terminal/src/terminal_element/terminal_layout_context.rs diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/connected_el.rs similarity index 57% rename from crates/terminal/src/terminal_element.rs rename to crates/terminal/src/connected_el.rs index 89618a9f3d..506e846e93 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/connected_el.rs @@ -1,5 +1,3 @@ -pub mod terminal_layout_context; - use alacritty_terminal::{ ansi::{Color::Named, NamedColor}, event::WindowSize, @@ -20,8 +18,7 @@ use gpui::{ json::json, text_layout::{Line, RunStyle}, Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, - PaintContext, Quad, ScrollWheelEvent, SizeConstraint, TextLayoutCache, WeakModelHandle, - WeakViewHandle, + PaintContext, Quad, ScrollWheelEvent, TextLayoutCache, WeakModelHandle, WeakViewHandle, }; use itertools::Itertools; use ordered_float::OrderedFloat; @@ -32,34 +29,58 @@ use util::ResultExt; use std::{cmp::min, ops::Range}; use std::{fmt::Debug, ops::Sub}; -use crate::{color_translation::convert_color, connection::Terminal, ConnectedView}; - -use self::terminal_layout_context::TerminalLayoutData; +use crate::{mappings::colors::convert_color, model::Terminal, ConnectedView}; ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I ///Implement scroll bars. const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; -///The GPUI element that paints the terminal. -///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? -pub struct TerminalEl { - terminal: WeakModelHandle, - view: WeakViewHandle, - modal: bool, +///The information generated during layout that is nescessary for painting +pub struct LayoutState { + cells: Vec, + rects: Vec, + highlights: Vec, + cursor: Option, + background_color: Color, + selection_color: Color, + size: TermDimensions, +} + +///Helper struct for converting data between alacritty's cursor points, and displayed cursor points +struct DisplayCursor { + line: i32, + col: usize, +} + +impl DisplayCursor { + fn from(cursor_point: Point, display_offset: usize) -> Self { + Self { + line: cursor_point.line.0 + display_offset as i32, + col: cursor_point.column.0, + } + } + + pub fn line(&self) -> i32 { + self.line + } + + pub fn col(&self) -> usize { + self.col + } } #[derive(Clone, Copy, Debug)] -pub struct TerminalDimensions { - pub cell_width: f32, - pub line_height: f32, - pub height: f32, - pub width: f32, +pub struct TermDimensions { + cell_width: f32, + line_height: f32, + height: f32, + width: f32, } -impl TerminalDimensions { +impl TermDimensions { pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self { - TerminalDimensions { + TermDimensions { cell_width, line_height, width: size.x(), @@ -92,8 +113,7 @@ impl TerminalDimensions { } } -//TODO look at what TermSize is -impl Into for TerminalDimensions { +impl Into for TermDimensions { fn into(self) -> WindowSize { WindowSize { num_lines: self.num_lines() as u16, @@ -104,9 +124,9 @@ impl Into for TerminalDimensions { } } -impl Dimensions for TerminalDimensions { +impl Dimensions for TermDimensions { fn total_lines(&self) -> usize { - self.num_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer... + self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer... } fn screen_lines(&self) -> usize { @@ -214,15 +234,12 @@ impl RelativeHighlightedRange { } } -///The information generated during layout that is nescessary for painting -pub struct LayoutState { - cells: Vec, - rects: Vec, - highlights: Vec, - cursor: Option, - background_color: Color, - selection_color: Color, - size: TerminalDimensions, +///The GPUI element that paints the terminal. +///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? +pub struct TerminalEl { + terminal: WeakModelHandle, + view: WeakViewHandle, + modal: bool, } impl TerminalEl { @@ -238,12 +255,173 @@ impl TerminalEl { } } + fn layout_grid( + grid: GridIterator, + text_style: &TextStyle, + terminal_theme: &TerminalStyle, + text_layout_cache: &TextLayoutCache, + modal: bool, + selection_range: Option, + ) -> ( + Vec, + Vec, + Vec, + ) { + let mut cells = vec![]; + let mut rects = vec![]; + let mut highlight_ranges = vec![]; + + let mut cur_rect: Option = None; + let mut cur_alac_color = None; + let mut highlighted_range = None; + + let linegroups = grid.group_by(|i| i.point.line); + for (line_index, (_, line)) in linegroups.into_iter().enumerate() { + for (x_index, cell) in line.enumerate() { + //Increase selection range + { + if selection_range + .map(|range| range.contains(cell.point)) + .unwrap_or(false) + { + let mut range = highlighted_range.take().unwrap_or(x_index..x_index); + range.end = range.end.max(x_index); + highlighted_range = Some(range); + } + } + + //Expand background rect range + { + if matches!(cell.bg, Named(NamedColor::Background)) { + //Continue to next cell, resetting variables if nescessary + cur_alac_color = None; + if let Some(rect) = cur_rect { + rects.push(rect); + cur_rect = None + } + } else { + match cur_alac_color { + Some(cur_color) => { + if cell.bg == cur_color { + cur_rect = cur_rect.take().map(|rect| rect.extend()); + } else { + cur_alac_color = Some(cell.bg); + if let Some(_) = cur_rect { + rects.push(cur_rect.take().unwrap()); + } + cur_rect = Some(LayoutRect::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + 1, + convert_color(&cell.bg, &terminal_theme.colors, modal), + )); + } + } + None => { + cur_alac_color = Some(cell.bg); + cur_rect = Some(LayoutRect::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + 1, + convert_color(&cell.bg, &terminal_theme.colors, modal), + )); + } + } + } + } + + //Layout current cell text + { + let cell_text = &cell.c.to_string(); + if cell_text != " " { + let cell_style = + TerminalEl::cell_style(&cell, terminal_theme, text_style, modal); + + let layout_cell = text_layout_cache.layout_str( + cell_text, + text_style.font_size, + &[(cell_text.len(), cell_style)], + ); + + cells.push(LayoutCell::new( + Point::new(line_index as i32, cell.point.column.0 as i32), + layout_cell, + )) + } + }; + } + + if highlighted_range.is_some() { + highlight_ranges.push(RelativeHighlightedRange::new( + line_index, + highlighted_range.take().unwrap(), + )) + } + + if cur_rect.is_some() { + rects.push(cur_rect.take().unwrap()); + } + } + + (cells, rects, highlight_ranges) + } + + // Compute the cursor position and expected block width, may return a zero width if x_for_index returns + // the same position for sequential indexes. Use em_width instead + fn shape_cursor( + cursor_point: DisplayCursor, + size: TermDimensions, + text_fragment: &Line, + ) -> Option<(Vector2F, f32)> { + if cursor_point.line() < size.total_lines() as i32 { + let cursor_width = if text_fragment.width() == 0. { + size.cell_width() + } else { + text_fragment.width() + }; + + Some(( + vec2f( + cursor_point.col() as f32 * size.cell_width(), + cursor_point.line() as f32 * size.line_height(), + ), + cursor_width, + )) + } else { + None + } + } + + ///Convert the Alacritty cell styles to GPUI text styles and background color + fn cell_style( + indexed: &Indexed<&Cell>, + style: &TerminalStyle, + text_style: &TextStyle, + modal: bool, + ) -> RunStyle { + let flags = indexed.cell.flags; + let fg = convert_color(&indexed.cell.fg, &style.colors, modal); + + let underline = flags + .contains(Flags::UNDERLINE) + .then(|| Underline { + color: Some(fg), + squiggly: false, + thickness: OrderedFloat(1.), + }) + .unwrap_or_default(); + + RunStyle { + color: fg, + font_id: text_style.font_id, + underline, + } + } + fn attach_mouse_handlers( &self, origin: Vector2F, view_id: usize, visible_bounds: RectF, - cur_size: TerminalDimensions, + cur_size: TermDimensions, cx: &mut PaintContext, ) { let mouse_down_connection = self.terminal.clone(); @@ -256,7 +434,7 @@ impl TerminalEl { move |MouseButtonEvent { position, .. }, cx| { if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) { conn_handle.update(cx.app, |terminal, cx| { - let (point, side) = mouse_to_cell_data( + let (point, side) = TerminalEl::mouse_to_cell_data( position, origin, cur_size, @@ -281,7 +459,7 @@ impl TerminalEl { cx.focus_parent_view(); if let Some(conn_handle) = click_connection.upgrade(cx.app) { conn_handle.update(cx.app, |terminal, cx| { - let (point, side) = mouse_to_cell_data( + let (point, side) = TerminalEl::mouse_to_cell_data( position, origin, cur_size, @@ -300,7 +478,7 @@ impl TerminalEl { move |_, MouseMovedEvent { position, .. }, cx| { if let Some(conn_handle) = drag_connection.upgrade(cx.app) { conn_handle.update(cx.app, |terminal, cx| { - let (point, side) = mouse_to_cell_data( + let (point, side) = TerminalEl::mouse_to_cell_data( position, origin, cur_size, @@ -316,6 +494,79 @@ impl TerminalEl { ), ); } + + ///Configures a text style from the current settings. + pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { + // Pull the font family from settings properly overriding + let family_id = settings + .terminal_overrides + .font_family + .as_ref() + .or_else(|| settings.terminal_defaults.font_family.as_ref()) + .and_then(|family_name| font_cache.load_family(&[family_name]).log_err()) + .unwrap_or(settings.buffer_font_family); + + let font_size = settings + .terminal_overrides + .font_size + .or(settings.terminal_defaults.font_size) + .unwrap_or(settings.buffer_font_size); + + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + + TextStyle { + color: settings.theme.editor.text_color, + font_family_id: family_id, + font_family_name: font_cache.family_name(family_id).unwrap(), + font_id, + font_size, + font_properties: Default::default(), + underline: Default::default(), + } + } + + pub fn mouse_to_cell_data( + pos: Vector2F, + origin: Vector2F, + cur_size: TermDimensions, + display_offset: usize, + ) -> (Point, alacritty_terminal::index::Direction) { + let pos = pos.sub(origin); + let point = { + let col = pos.x() / cur_size.cell_width; //TODO: underflow... + let col = min(GridCol(col as usize), cur_size.last_column()); + + let line = pos.y() / cur_size.line_height; + let line = min(line as i32, cur_size.bottommost_line().0); + + Point::new(GridLine(line - display_offset as i32), col) + }; + + //Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() + let side = { + let x = pos.0.x() as usize; + let cell_x = + x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize; + let half_cell_width = (cur_size.cell_width / 2.0) as usize; + + let additional_padding = + (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width; + let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding; + //Width: Pixels or columns? + if cell_x > half_cell_width + // Edge case when mouse leaves the window. + || x as f32 >= end_of_grid + { + Side::Right + } else { + Side::Left + } + }; + + (point, side) + } } impl Element for TerminalEl { @@ -327,40 +578,74 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let layout = - TerminalLayoutData::new(cx.global::(), &cx.font_cache(), constraint.max); + let settings = cx.global::(); + let font_cache = &cx.font_cache(); + + //Setup layout information + let terminal_theme = &settings.theme.terminal; + let text_style = TerminalEl::make_text_style(font_cache, &settings); + let selection_color = settings.theme.editor.selection.selection; + let dimensions = { + let line_height = font_cache.line_height(text_style.font_size); + let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); + TermDimensions::new(line_height, cell_width, constraint.max) + }; let terminal = self.terminal.upgrade(cx).unwrap().read(cx); let (cursor, cells, rects, highlights) = - terminal.render_lock(Some(layout.size.clone()), |content, cursor_text| { - let (cells, rects, highlights) = layout_grid( + terminal.render_lock(Some(dimensions.clone()), |content, cursor_text| { + let (cells, rects, highlights) = TerminalEl::layout_grid( content.display_iter, - &layout.text_style, - layout.terminal_theme, + &text_style, + terminal_theme, cx.text_layout_cache, self.modal, content.selection, ); //Layout cursor - let cursor = layout_cursor( - cursor_text, - cx.text_layout_cache, - &layout, - content.cursor.point, - content.display_offset, - constraint, - ); + let cursor = { + let cursor_point = + DisplayCursor::from(content.cursor.point, content.display_offset); + let cursor_text = { + let str_trxt = cursor_text.to_string(); + cx.text_layout_cache.layout_str( + &str_trxt, + text_style.font_size, + &[( + str_trxt.len(), + RunStyle { + font_id: text_style.font_id, + color: terminal_theme.colors.background, + underline: Default::default(), + }, + )], + ) + }; + + TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map( + move |(cursor_position, block_width)| { + Cursor::new( + cursor_position, + block_width, + dimensions.line_height, + terminal_theme.colors.cursor, + CursorShape::Block, + Some(cursor_text.clone()), + ) + }, + ) + }; (cursor, cells, rects, highlights) }); //Select background color let background_color = if self.modal { - layout.terminal_theme.colors.modal_background + terminal_theme.colors.modal_background } else { - layout.terminal_theme.colors.background + terminal_theme.colors.background }; //Done! @@ -370,8 +655,8 @@ impl Element for TerminalEl { cells, cursor, background_color, - selection_color: layout.selection_color, - size: layout.size, + selection_color, + size: dimensions, rects, highlights, }, @@ -385,13 +670,6 @@ impl Element for TerminalEl { layout: &mut Self::LayoutState, cx: &mut gpui::PaintContext, ) -> Self::PaintState { - /* - * For paint, I want to change how mouse events are handled: - * - Refactor the mouse handlers to push the grid cell actions into the connection - * - But keep the conversion from GPUI coordinates to grid cells in the Terminal element - * - Switch from directly painting things, to calling 'paint' on items produced by layout - */ - //Setup element stuff let clip_bounds = Some(visible_bounds); @@ -520,270 +798,6 @@ impl Element for TerminalEl { } } -///TODO: Fix cursor rendering with alacritty fork -fn layout_cursor( - cursor_text: char, - text_layout_cache: &TextLayoutCache, - tcx: &TerminalLayoutData, - cursor_point: Point, - display_offset: usize, - constraint: SizeConstraint, -) -> Option { - let cursor_text = layout_cursor_text(cursor_text, cursor_point, text_layout_cache, tcx); - get_cursor_shape( - cursor_point.line.0 as usize, - cursor_point.column.0 as usize, - display_offset, - tcx.size.line_height, - tcx.size.cell_width, - (constraint.max.y() / tcx.size.line_height) as usize, //TODO - &cursor_text, - ) - .map(move |(cursor_position, block_width)| { - let block_width = if block_width != 0.0 { - block_width - } else { - tcx.size.cell_width - }; - - Cursor::new( - cursor_position, - block_width, - tcx.size.line_height, - tcx.terminal_theme.colors.cursor, - CursorShape::Block, - Some(cursor_text.clone()), - ) - }) -} - -fn layout_cursor_text( - cursor_text: char, - _cursor_point: Point, - text_layout_cache: &TextLayoutCache, - tcx: &TerminalLayoutData, -) -> Line { - let cursor_text = cursor_text.to_string(); - text_layout_cache.layout_str( - &cursor_text, - tcx.text_style.font_size, - &[( - cursor_text.len(), - RunStyle { - font_id: tcx.text_style.font_id, - color: tcx.terminal_theme.colors.background, - underline: Default::default(), - }, - )], - ) -} - -pub fn mouse_to_cell_data( - pos: Vector2F, - origin: Vector2F, - cur_size: TerminalDimensions, - display_offset: usize, -) -> (Point, alacritty_terminal::index::Direction) { - let pos = pos.sub(origin); - let point = { - let col = pos.x() / cur_size.cell_width; //TODO: underflow... - let col = min(GridCol(col as usize), cur_size.last_column()); - - let line = pos.y() / cur_size.line_height; - let line = min(line as i32, cur_size.bottommost_line().0); - - Point::new(GridLine(line - display_offset as i32), col) - }; - - //Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() - let side = { - let x = pos.0.x() as usize; - let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize; - let half_cell_width = (cur_size.cell_width / 2.0) as usize; - - let additional_padding = - (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width; - let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding; - //Width: Pixels or columns? - if cell_x > half_cell_width - // Edge case when mouse leaves the window. - || x as f32 >= end_of_grid - { - Side::Right - } else { - Side::Left - } - }; - - (point, side) -} - -fn layout_grid( - grid: GridIterator, - text_style: &TextStyle, - terminal_theme: &TerminalStyle, - text_layout_cache: &TextLayoutCache, - modal: bool, - selection_range: Option, -) -> ( - Vec, - Vec, - Vec, -) { - let mut cells = vec![]; - let mut rects = vec![]; - let mut highlight_ranges = vec![]; - - let mut cur_rect: Option = None; - let mut cur_alac_color = None; - let mut highlighted_range = None; - - let linegroups = grid.group_by(|i| i.point.line); - for (line_index, (_, line)) in linegroups.into_iter().enumerate() { - for (x_index, cell) in line.enumerate() { - //Increase selection range - { - if selection_range - .map(|range| range.contains(cell.point)) - .unwrap_or(false) - { - let mut range = highlighted_range.take().unwrap_or(x_index..x_index); - range.end = range.end.max(x_index); - highlighted_range = Some(range); - } - } - - //Expand background rect range - { - if matches!(cell.bg, Named(NamedColor::Background)) { - //Continue to next cell, resetting variables if nescessary - cur_alac_color = None; - if let Some(rect) = cur_rect { - rects.push(rect); - cur_rect = None - } - } else { - match cur_alac_color { - Some(cur_color) => { - if cell.bg == cur_color { - cur_rect = cur_rect.take().map(|rect| rect.extend()); - } else { - cur_alac_color = Some(cell.bg); - if let Some(_) = cur_rect { - rects.push(cur_rect.take().unwrap()); - } - cur_rect = Some(LayoutRect::new( - Point::new(line_index as i32, cell.point.column.0 as i32), - 1, - convert_color(&cell.bg, &terminal_theme.colors, modal), - )); - } - } - None => { - cur_alac_color = Some(cell.bg); - cur_rect = Some(LayoutRect::new( - Point::new(line_index as i32, cell.point.column.0 as i32), - 1, - convert_color(&cell.bg, &terminal_theme.colors, modal), - )); - } - } - } - } - - //Layout current cell text - { - let cell_text = &cell.c.to_string(); - if cell_text != " " { - let cell_style = cell_style(&cell, terminal_theme, text_style, modal); - - let layout_cell = text_layout_cache.layout_str( - cell_text, - text_style.font_size, - &[(cell_text.len(), cell_style)], - ); - - cells.push(LayoutCell::new( - Point::new(line_index as i32, cell.point.column.0 as i32), - layout_cell, - )) - } - }; - } - - if highlighted_range.is_some() { - highlight_ranges.push(RelativeHighlightedRange::new( - line_index, - highlighted_range.take().unwrap(), - )) - } - - if cur_rect.is_some() { - rects.push(cur_rect.take().unwrap()); - } - } - - (cells, rects, highlight_ranges) -} - -// Compute the cursor position and expected block width, may return a zero width if x_for_index returns -// the same position for sequential indexes. Use em_width instead -//TODO: This function is messy, too many arguments and too many ifs. Simplify. -fn get_cursor_shape( - line: usize, - line_index: usize, - display_offset: usize, - line_height: f32, - cell_width: f32, - total_lines: usize, - text_fragment: &Line, -) -> Option<(Vector2F, f32)> { - let cursor_line = line + display_offset; - if cursor_line <= total_lines { - let cursor_width = if text_fragment.width() == 0. { - cell_width - } else { - text_fragment.width() - }; - - Some(( - vec2f( - line_index as f32 * cell_width, - cursor_line as f32 * line_height, - ), - cursor_width, - )) - } else { - None - } -} - -///Convert the Alacritty cell styles to GPUI text styles and background color -fn cell_style( - indexed: &Indexed<&Cell>, - style: &TerminalStyle, - text_style: &TextStyle, - modal: bool, -) -> RunStyle { - let flags = indexed.cell.flags; - let fg = convert_color(&indexed.cell.fg, &style.colors, modal); - - let underline = flags - .contains(Flags::UNDERLINE) - .then(|| Underline { - color: Some(fg), - squiggly: false, - thickness: OrderedFloat(1.), - }) - .unwrap_or_default(); - - RunStyle { - color: fg, - font_id: text_style.font_id, - underline, - } -} - mod test { #[test] @@ -797,7 +811,7 @@ mod test { let origin_x = 10.; let origin_y = 20.; - let cur_size = crate::terminal_element::TerminalDimensions::new( + let cur_size = crate::connected_el::TermDimensions::new( line_height, cell_width, gpui::geometry::vector::vec2f(term_width, term_height), @@ -806,7 +820,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 (point, _) = - crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); + crate::connected_el::TerminalEl::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); assert_eq!( point, alacritty_terminal::index::Point::new( diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs new file mode 100644 index 0000000000..633bd70b5a --- /dev/null +++ b/crates/terminal/src/connected_view.rs @@ -0,0 +1,162 @@ +use gpui::{ + actions, keymap::Keystroke, ClipboardItem, Element, ElementBox, ModelHandle, MutableAppContext, + View, ViewContext, +}; + +use crate::{ + connected_el::TerminalEl, + model::{Event, Terminal}, +}; + +///Event to transmit the scroll from the element to the view +#[derive(Clone, Debug, PartialEq)] +pub struct ScrollTerminal(pub i32); + +actions!( + terminal, + [Up, Down, CtrlC, Escape, Enter, Clear, Copy, Paste,] +); + +pub fn init(cx: &mut MutableAppContext) { + //Global binding overrrides + cx.add_action(ConnectedView::ctrl_c); + cx.add_action(ConnectedView::up); + cx.add_action(ConnectedView::down); + cx.add_action(ConnectedView::escape); + cx.add_action(ConnectedView::enter); + //Useful terminal views + cx.add_action(ConnectedView::copy); + cx.add_action(ConnectedView::paste); + cx.add_action(ConnectedView::clear); +} + +///A terminal view, maintains the PTY's file handles and communicates with the terminal +pub struct ConnectedView { + terminal: ModelHandle, + has_new_content: bool, + //Currently using iTerm bell, show bell emoji in tab until input is received + has_bell: bool, + // Only for styling purposes. Doesn't effect behavior + modal: bool, +} + +impl ConnectedView { + pub fn from_terminal( + terminal: ModelHandle, + modal: bool, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); + cx.subscribe(&terminal, |this, _, event, cx| match event { + Event::Wakeup => { + if cx.is_self_focused() { + cx.notify() + } else { + this.has_new_content = true; + cx.emit(Event::TitleChanged); + } + } + Event::Bell => { + this.has_bell = true; + cx.emit(Event::TitleChanged); + } + _ => cx.emit(*event), + }) + .detach(); + + Self { + terminal, + has_new_content: true, + has_bell: false, + modal, + } + } + + pub fn handle(&self) -> ModelHandle { + self.terminal.clone() + } + + pub fn has_new_content(&self) -> bool { + self.has_new_content + } + + pub fn has_bell(&self) -> bool { + self.has_bell + } + + pub fn clear_bel(&mut self, cx: &mut ViewContext) { + self.has_bell = false; + cx.emit(Event::TitleChanged); + } + + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { + self.terminal.read(cx).clear(); + } + + ///Attempt to paste the clipboard into the terminal + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + self.terminal + .read(cx) + .copy() + .map(|text| cx.write_to_clipboard(ClipboardItem::new(text))); + } + + ///Attempt to paste the clipboard into the terminal + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + cx.read_from_clipboard().map(|item| { + self.terminal.read(cx).paste(item.text()); + }); + } + + ///Synthesize the keyboard event corresponding to 'up' + fn up(&mut self, _: &Up, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("up").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'down' + fn down(&mut self, _: &Down, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("down").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'ctrl-c' + fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'escape' + fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("escape").unwrap()); + } + + ///Synthesize the keyboard event corresponding to 'enter' + fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("enter").unwrap()); + } +} + +impl View for ConnectedView { + fn ui_name() -> &'static str { + "Connected Terminal View" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let terminal_handle = self.terminal.clone().downgrade(); + TerminalEl::new(cx.handle(), terminal_handle, self.modal) + .contained() + .boxed() + } + + fn on_focus(&mut self, _cx: &mut ViewContext) { + self.has_new_content = false; + } +} diff --git a/crates/terminal/src/color_translation.rs b/crates/terminal/src/mappings/colors.rs similarity index 98% rename from crates/terminal/src/color_translation.rs rename to crates/terminal/src/mappings/colors.rs index 946a22d304..1a425ebaed 100644 --- a/crates/terminal/src/color_translation.rs +++ b/crates/terminal/src/mappings/colors.rs @@ -133,7 +133,7 @@ mod tests { fn test_rgb_for_index() { //Test every possible value in the color cube for i in 16..=231 { - let (r, g, b) = crate::color_translation::rgb_for_index(&(i as u8)); + let (r, g, b) = crate::mappings::colors::rgb_for_index(&(i as u8)); assert_eq!(i, 16 + 36 * r + 6 * g + b); } } diff --git a/crates/terminal/src/connection/keymappings.rs b/crates/terminal/src/mappings/keys.rs similarity index 99% rename from crates/terminal/src/connection/keymappings.rs rename to crates/terminal/src/mappings/keys.rs index a4d429843b..51d02d6bb2 100644 --- a/crates/terminal/src/connection/keymappings.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -1,15 +1,6 @@ use alacritty_terminal::term::TermMode; use gpui::keymap::Keystroke; -/* -Connection events still to do: -- Reporting mouse events correctly. -- Reporting scrolls -- Correctly bracketing a paste -- Storing changed colors -- Focus change sequence -*/ - #[derive(Debug)] pub enum Modifiers { None, diff --git a/crates/terminal/src/mappings/mod.rs b/crates/terminal/src/mappings/mod.rs new file mode 100644 index 0000000000..cde6c337ea --- /dev/null +++ b/crates/terminal/src/mappings/mod.rs @@ -0,0 +1,2 @@ +pub mod colors; +pub mod keys; diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal_view.rs similarity index 91% rename from crates/terminal/src/modal.rs rename to crates/terminal/src/modal_view.rs index c53f2a0469..ec5280befc 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal_view.rs @@ -2,7 +2,7 @@ use gpui::{ModelHandle, ViewContext}; use workspace::Workspace; use crate::{ - connection::Terminal, get_working_directory, DeployModal, Event, TerminalContent, TerminalView, + get_working_directory, model::Terminal, DeployModal, Event, TerminalContent, TerminalView, }; #[derive(Debug)] @@ -32,7 +32,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon let this = cx.add_view(|cx| TerminalView::new(working_directory, true, cx)); if let TerminalContent::Connected(connected) = &this.read(cx).content { - let terminal_handle = connected.read(cx).terminal.clone(); + let terminal_handle = connected.read(cx).handle(); cx.subscribe(&terminal_handle, on_event).detach(); // Set the global immediately if terminal construction was successful, // in case the user opens the command palette @@ -46,7 +46,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon // Terminal modal was dismissed. Store terminal if the terminal view is connected if let TerminalContent::Connected(connected) = &closed_terminal_handle.read(cx).content { - let terminal_handle = connected.read(cx).terminal.clone(); + let terminal_handle = connected.read(cx).handle(); // Set the global immediately if terminal construction was successful, // in case the user opens the command palette cx.set_global::>(Some(StoredTerminal( diff --git a/crates/terminal/src/connection.rs b/crates/terminal/src/model.rs similarity index 98% rename from crates/terminal/src/connection.rs rename to crates/terminal/src/model.rs index d41f4e8fad..852dc555c5 100644 --- a/crates/terminal/src/connection.rs +++ b/crates/terminal/src/model.rs @@ -1,5 +1,3 @@ -mod keymappings; - use alacritty_terminal::{ ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig}, @@ -25,12 +23,13 @@ use thiserror::Error; use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; use crate::{ - color_translation::{get_color_at_index, to_alac_rgb}, - terminal_element::TerminalDimensions, + connected_el::TermDimensions, + mappings::{ + colors::{get_color_at_index, to_alac_rgb}, + keys::to_esc_str, + }, }; -use self::keymappings::to_esc_str; - const DEFAULT_TITLE: &str = "Terminal"; ///Upward flowing events, for changing the title and such @@ -132,7 +131,7 @@ impl TerminalBuilder { working_directory: Option, shell: Option, env: Option>, - initial_size: TerminalDimensions, + initial_size: TermDimensions, ) -> Result { let pty_config = { let alac_shell = shell.clone().and_then(|shell| match shell { @@ -364,7 +363,7 @@ impl Terminal { self.term.lock().selection = sel; } - pub fn render_lock(&self, new_size: Option, f: F) -> T + pub fn render_lock(&self, new_size: Option, f: F) -> T where F: FnOnce(RenderableContent, char) -> T, { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index ab543a22dc..ca10506cdd 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,64 +1,40 @@ -mod color_translation; -pub mod connection; -mod modal; -pub mod terminal_element; +pub mod connected_el; +pub mod connected_view; +pub mod mappings; +pub mod modal_view; +pub mod model; -use connection::{Event, Terminal, TerminalBuilder, TerminalError}; +use connected_view::ConnectedView; use dirs::home_dir; use gpui::{ - actions, elements::*, geometry::vector::vec2f, keymap::Keystroke, AnyViewHandle, AppContext, - ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, + actions, elements::*, geometry::vector::vec2f, AnyViewHandle, AppContext, Entity, ModelHandle, + MutableAppContext, View, ViewContext, ViewHandle, }; -use modal::deploy_modal; +use modal_view::deploy_modal; +use model::{Event, Terminal, TerminalBuilder, TerminalError}; +use connected_el::TermDimensions; use project::{LocalWorktree, Project, ProjectPath}; use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; use std::path::{Path, PathBuf}; -use terminal_element::{terminal_layout_context::TerminalLayoutData, TerminalDimensions}; use workspace::{Item, Workspace}; -use crate::terminal_element::TerminalEl; +use crate::connected_el::TerminalEl; const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space. const DEBUG_TERMINAL_HEIGHT: f32 = 200.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; -///Event to transmit the scroll from the element to the view -#[derive(Clone, Debug, PartialEq)] -pub struct ScrollTerminal(pub i32); - -actions!( - terminal, - [ - Deploy, - Up, - Down, - CtrlC, - Escape, - Enter, - Clear, - Copy, - Paste, - DeployModal - ] -); +actions!(terminal, [Deploy, DeployModal]); ///Initialize and register all of our action handlers pub fn init(cx: &mut MutableAppContext) { - //Global binding overrrides - cx.add_action(ConnectedView::ctrl_c); - cx.add_action(ConnectedView::up); - cx.add_action(ConnectedView::down); - cx.add_action(ConnectedView::escape); - cx.add_action(ConnectedView::enter); - //Useful terminal actions - cx.add_action(ConnectedView::deploy); - cx.add_action(ConnectedView::copy); - cx.add_action(ConnectedView::paste); - cx.add_action(ConnectedView::clear); + cx.add_action(TerminalView::deploy); cx.add_action(deploy_modal); + + connected_view::init(cx); } //Make terminal view an enum, that can give you views for the error and non-error states @@ -88,16 +64,6 @@ pub struct ErrorView { error: TerminalError, } -///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct ConnectedView { - terminal: ModelHandle, - has_new_content: bool, - //Currently using iTerm bell, show bell emoji in tab until input is received - has_bell: bool, - // Only for styling purposes. Doesn't effect behavior - modal: bool, -} - impl Entity for TerminalView { type Event = Event; } @@ -111,11 +77,18 @@ impl Entity for ErrorView { } impl TerminalView { + ///Create a new Terminal in the current working directory or the user's home directory + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + let working_directory = get_working_directory(workspace, cx); + let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); + workspace.add_item(Box::new(view), cx); + } + ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` fn new(working_directory: Option, modal: bool, cx: &mut ViewContext) -> Self { //The details here don't matter, the terminal will be resized on the first layout - let size_info = TerminalDimensions::new( + let size_info = TermDimensions::new( DEBUG_LINE_HEIGHT, DEBUG_CELL_WIDTH, vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), @@ -194,122 +167,6 @@ impl View for TerminalView { } } -impl ConnectedView { - fn from_terminal( - terminal: ModelHandle, - modal: bool, - cx: &mut ViewContext, - ) -> Self { - cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&terminal, |this, _, event, cx| match event { - Event::Wakeup => { - if cx.is_self_focused() { - cx.notify() - } else { - this.has_new_content = true; - cx.emit(Event::TitleChanged); - } - } - Event::Bell => { - this.has_bell = true; - cx.emit(Event::TitleChanged); - } - _ => cx.emit(*event), - }) - .detach(); - - Self { - terminal, - has_new_content: true, - has_bell: false, - modal, - } - } - - fn clear_bel(&mut self, cx: &mut ViewContext) { - self.has_bell = false; - cx.emit(Event::TitleChanged); - } - - ///Create a new Terminal in the current working directory or the user's home directory - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - let working_directory = get_working_directory(workspace, cx); - let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); - workspace.add_item(Box::new(view), cx); - } - - fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { - self.terminal.read(cx).clear(); - } - - ///Attempt to paste the clipboard into the terminal - fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - self.terminal - .read(cx) - .copy() - .map(|text| cx.write_to_clipboard(ClipboardItem::new(text))); - } - - ///Attempt to paste the clipboard into the terminal - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - cx.read_from_clipboard().map(|item| { - self.terminal.read(cx).paste(item.text()); - }); - } - - ///Synthesize the keyboard event corresponding to 'up' - fn up(&mut self, _: &Up, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("up").unwrap()); - } - - ///Synthesize the keyboard event corresponding to 'down' - fn down(&mut self, _: &Down, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("down").unwrap()); - } - - ///Synthesize the keyboard event corresponding to 'ctrl-c' - fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); - } - - ///Synthesize the keyboard event corresponding to 'escape' - fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("escape").unwrap()); - } - - ///Synthesize the keyboard event corresponding to 'enter' - fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("enter").unwrap()); - } -} - -impl View for ConnectedView { - fn ui_name() -> &'static str { - "Connected Terminal View" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let terminal_handle = self.terminal.clone().downgrade(); - TerminalEl::new(cx.handle(), terminal_handle, self.modal) - .contained() - .boxed() - } - - fn on_focus(&mut self, _cx: &mut ViewContext) { - self.has_new_content = false; - } -} - impl View for ErrorView { fn ui_name() -> &'static str { "Terminal Error" @@ -317,7 +174,7 @@ impl View for ErrorView { fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { let settings = cx.global::(); - let style = TerminalLayoutData::make_text_style(cx.font_cache(), settings); + let style = TerminalEl::make_text_style(cx.font_cache(), settings); Label::new( format!( @@ -341,7 +198,7 @@ impl Item for TerminalView { ) -> ElementBox { let title = match &self.content { TerminalContent::Connected(connected) => { - connected.read(cx).terminal.read(cx).title.clone() + connected.read(cx).handle().read(cx).title.clone() } TerminalContent::Error(_) => "Terminal".to_string(), }; @@ -363,7 +220,7 @@ impl Item for TerminalView { if let TerminalContent::Connected(connected) = &self.content { let associated_directory = connected .read(cx) - .terminal + .handle() .read(cx) .associated_directory .clone(); @@ -418,7 +275,7 @@ impl Item for TerminalView { fn is_dirty(&self, cx: &gpui::AppContext) -> bool { if let TerminalContent::Connected(connected) = &self.content { - connected.read(cx).has_new_content + connected.read(cx).has_new_content() } else { false } @@ -426,7 +283,7 @@ impl Item for TerminalView { fn has_conflict(&self, cx: &AppContext) -> bool { if let TerminalContent::Connected(connected) = &self.content { - connected.read(cx).has_bell + connected.read(cx).has_bell() } else { false } diff --git a/crates/terminal/src/terminal_element/terminal_layout_context.rs b/crates/terminal/src/terminal_element/terminal_layout_context.rs deleted file mode 100644 index 6128d17828..0000000000 --- a/crates/terminal/src/terminal_element/terminal_layout_context.rs +++ /dev/null @@ -1,60 +0,0 @@ -use super::*; - -pub struct TerminalLayoutData<'a> { - pub text_style: TextStyle, - pub selection_color: Color, - pub terminal_theme: &'a TerminalStyle, - pub size: TerminalDimensions, -} - -impl<'a> TerminalLayoutData<'a> { - pub fn new(settings: &'a Settings, font_cache: &FontCache, constraint: Vector2F) -> Self { - let text_style = Self::make_text_style(font_cache, &settings); - let selection_color = settings.theme.editor.selection.selection; - let terminal_theme = &settings.theme.terminal; - - let line_height = font_cache.line_height(text_style.font_size); - - let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); - let dimensions = TerminalDimensions::new(line_height, cell_width, constraint); - - TerminalLayoutData { - size: dimensions, - text_style, - selection_color, - terminal_theme, - } - } - - ///Configures a text style from the current settings. - pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { - // Pull the font family from settings properly overriding - let family_id = settings - .terminal_overrides - .font_family - .as_ref() - .or_else(|| settings.terminal_defaults.font_family.as_ref()) - .and_then(|family_name| font_cache.load_family(&[family_name]).log_err()) - .unwrap_or(settings.buffer_font_family); - - let font_size = settings - .terminal_overrides - .font_size - .or(settings.terminal_defaults.font_size) - .unwrap_or(settings.buffer_font_size); - - let font_id = font_cache - .select_font(family_id, &Default::default()) - .unwrap(); - - TextStyle { - color: settings.theme.editor.text_color, - font_family_id: family_id, - font_family_name: font_cache.family_name(family_id).unwrap(), - font_id, - font_size, - font_properties: Default::default(), - underline: Default::default(), - } - } -} diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index f9b99d60d8..e78939224b 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -8,8 +8,8 @@ use project::{Entry, Project, ProjectPath, Worktree}; use workspace::{AppState, Workspace}; use crate::{ - connection::{Terminal, TerminalBuilder}, - terminal_element::TerminalDimensions, + connected_el::TermDimensions, + model::{Terminal, TerminalBuilder}, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH, }; @@ -22,7 +22,7 @@ impl<'a> TerminalTestContext<'a> { pub fn new(cx: &'a mut TestAppContext, term: bool) -> Self { cx.set_condition_duration(Some(Duration::from_secs(5))); - let size_info = TerminalDimensions::new( + let size_info = TermDimensions::new( DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), From a41e54f3dcea31d37a358699029be796fd4b752a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 22 Jul 2022 13:54:46 -0700 Subject: [PATCH 29/29] Finished terminal refactoring --- crates/terminal/src/model.rs | 44 +++++++++++++-------- crates/terminal/src/terminal.rs | 70 ++++++++++++++++++++++----------- 2 files changed, 75 insertions(+), 39 deletions(-) diff --git a/crates/terminal/src/model.rs b/crates/terminal/src/model.rs index 852dc555c5..f1b2dd36cf 100644 --- a/crates/terminal/src/model.rs +++ b/crates/terminal/src/model.rs @@ -60,10 +60,9 @@ pub struct TerminalError { pub source: std::io::Error, } -impl Display for TerminalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let dir_string: String = self - .directory +impl TerminalError { + pub fn fmt_directory(&self) -> String { + self.directory .clone() .map(|path| { match path @@ -79,13 +78,22 @@ impl Display for TerminalError { let default_dir = dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string()); match default_dir { - Some(dir) => format!(" {}", dir), - None => "".to_string(), + Some(dir) => format!(" {}", dir), + None => "".to_string(), } - }); + }) + } - let shell = self - .shell + pub fn shell_to_string(&self) -> Option { + self.shell.as_ref().map(|shell| match shell { + Shell::System => "".to_string(), + Shell::Program(p) => p.to_string(), + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + }) + } + + pub fn fmt_shell(&self) -> String { + self.shell .clone() .map(|shell| match shell { Shell::System => { @@ -94,7 +102,7 @@ impl Display for TerminalError { match pw { Some(pw) => format!(" {}", pw.shell), - None => "".to_string(), + None => "".to_string(), } } Shell::Program(s) => s, @@ -107,11 +115,17 @@ impl Display for TerminalError { Some(pw) => { format!(" {}", pw.shell) } - None => { - " {}".to_string() - } + None => " {}".to_string(), } - }); + }) + } +} + +impl Display for TerminalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let dir_string: String = self.fmt_directory(); + + let shell = self.fmt_shell(); write!( f, @@ -210,7 +224,6 @@ impl TerminalBuilder { pty_tx: Notifier(pty_tx), term, title: shell_txt.to_string(), - associated_directory: working_directory, }; Ok(TerminalBuilder { @@ -245,7 +258,6 @@ pub struct Terminal { pty_tx: Notifier, term: Arc>>, pub title: String, - pub associated_directory: Option, } impl Terminal { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index ca10506cdd..2f5ef5ffab 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -58,6 +58,7 @@ impl TerminalContent { pub struct TerminalView { modal: bool, content: TerminalContent, + associated_directory: Option, } pub struct ErrorView { @@ -98,7 +99,8 @@ impl TerminalView { let shell = settings.terminal_overrides.shell.clone(); let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. - let content = match TerminalBuilder::new(working_directory, shell, envs, size_info) { + let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info) + { Ok(terminal) => { let terminal = cx.add_model(|cx| terminal.subscribe(cx)); let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); @@ -115,7 +117,11 @@ impl TerminalView { }; cx.focus(content.handle()); - TerminalView { modal, content } + TerminalView { + modal, + content, + associated_directory: working_directory, + } } fn from_terminal( @@ -127,6 +133,7 @@ impl TerminalView { TerminalView { modal, content: TerminalContent::Connected(connected_view), + associated_directory: None, } } } @@ -176,16 +183,39 @@ impl View for ErrorView { let settings = cx.global::(); let style = TerminalEl::make_text_style(cx.font_cache(), settings); - Label::new( - format!( - "Failed to open the terminal. Info: \n{}", - self.error.to_string() - ), - style, - ) - .aligned() - .contained() - .boxed() + //TODO: + //We want markdown style highlighting so we can format the program and working directory with `` + //We want a max-width of 75% with word-wrap + //We want to be able to select the text + //Want to be able to scroll if the error message is massive somehow (resiliency) + + let program_text = { + match self.error.shell_to_string() { + Some(shell_txt) => format!("Shell Program: `{}`", shell_txt), + None => "No program specified".to_string(), + } + }; + + let directory_text = { + match self.error.directory.as_ref() { + Some(path) => format!("Working directory: `{}`", path.to_string_lossy()), + None => "No working directory specified".to_string(), + } + }; + + let error_text = self.error.source.to_string(); + + Flex::column() + .with_child( + Text::new("Failed to open the terminal.".to_string(), style.clone()) + .contained() + .boxed(), + ) + .with_child(Text::new(program_text, style.clone()).contained().boxed()) + .with_child(Text::new(directory_text, style.clone()).contained().boxed()) + .with_child(Text::new(error_text, style.clone()).contained().boxed()) + .aligned() + .boxed() } } @@ -217,17 +247,11 @@ impl Item for TerminalView { //From what I can tell, there's no way to tell the current working //Directory of the terminal from outside the shell. There might be //solutions to this, but they are non-trivial and require more IPC - if let TerminalContent::Connected(connected) = &self.content { - let associated_directory = connected - .read(cx) - .handle() - .read(cx) - .associated_directory - .clone(); - Some(TerminalView::new(associated_directory, false, cx)) - } else { - None - } + Some(TerminalView::new( + self.associated_directory.clone(), + false, + cx, + )) } fn project_path(&self, _cx: &gpui::AppContext) -> Option {