diff --git a/Cargo.lock b/Cargo.lock index 97bfa159942326ba0640933b7b20415dd90fd75e..8e9e976efd7edc431ea897a574c79885f2bf5f92 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=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a" 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=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a" dependencies = [ "alacritty_config_derive", "base64 0.13.0", "bitflags", - "dirs 3.0.2", + "dirs 4.0.0", "libc", "log", "mio 0.6.23", @@ -5355,12 +5353,14 @@ name = "terminal" version = "0.1.0" dependencies = [ "alacritty_terminal", + "anyhow", "client", "dirs 4.0.0", "editor", "futures", "gpui", "itertools", + "libc", "mio-extras", "ordered-float", "project", @@ -5368,6 +5368,7 @@ dependencies = [ "shellexpand", "smallvec", "theme", + "thiserror", "util", "workspace", ] diff --git a/assets/settings/default.json b/assets/settings/default.json index 73c73636f69d048dc55e4365d4492e7a07f77470..6c34d6be70ebd3193469814e628f6c136335a9d8 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/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 467a45b93efac3f2126ee21b0e482357917f51f4..5f438057ee96440c7cce45e105cbb1e7f7d457c0 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 720e0142beb35684fc4afad10fd7e7f5e0545e00..c8172e0de3e59b07a89235f790a238724a52ca5e 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/settings/src/settings.rs b/crates/settings/src/settings.rs index 76cc653ff8b65b334974743c9b9b6e5cee775073..c5188884562899afe3a68a4c6febc3c6a562e77e 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 09a3fb171f2fd0b277dfabb98b57d818692fb442..03c6a26b7db643322c3a3cd9cc98bcce7cba7310 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 = "e9b864860ec79cc1b70042aafce100cdd6985a0a"} editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } @@ -23,6 +23,10 @@ ordered-float = "2.1.1" itertools = "0.10" dirs = "4.0.0" shellexpand = "2.1.0" +libc = "0.2" +anyhow = "1" +thiserror = "1.0" + [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs new file mode 100644 index 0000000000000000000000000000000000000000..506e846e93564cbc05b78e02e2375f14010bf225 --- /dev/null +++ b/crates/terminal/src/connected_el.rs @@ -0,0 +1,832 @@ +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}, +}; +use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; +use gpui::{ + color::Color, + elements::*, + fonts::{TextStyle, Underline}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::json, + text_layout::{Line, RunStyle}, + Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, + PaintContext, Quad, ScrollWheelEvent, TextLayoutCache, WeakModelHandle, WeakViewHandle, +}; +use itertools::Itertools; +use ordered_float::OrderedFloat; +use settings::Settings; +use theme::TerminalStyle; +use util::ResultExt; + +use std::{cmp::min, ops::Range}; +use std::{fmt::Debug, ops::Sub}; + +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 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 TermDimensions { + cell_width: f32, + line_height: f32, + height: f32, + width: f32, +} + +impl TermDimensions { + pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self { + TermDimensions { + 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 + } +} + +impl Into for TermDimensions { + 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 TermDimensions { + fn total_lines(&self) -> usize { + self.screen_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 { + 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.size.line_height, 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.size.cell_width.ceil() * self.num_of_cells as f32).ceil(), + layout.size.line_height, + ); + + 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.size.cell_width).floor(), + origin.y() + point.line as f32 * layout.size.line_height, + ) +} + +#[derive(Clone, Debug, Default)] +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.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 }; + } +} + +///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 { + pub fn new( + view: WeakViewHandle, + terminal: WeakModelHandle, + modal: bool, + ) -> TerminalEl { + TerminalEl { + view, + terminal, + modal, + } + } + + 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: TermDimensions, + cx: &mut PaintContext, + ) { + 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( + MouseButton::Left, + 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) = TerminalEl::mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); + + terminal.mouse_down(point, side); + + cx.notify(); + }) + } + }, + ) + .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, |terminal, cx| { + let (point, side) = TerminalEl::mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); + + terminal.click(point, side, click_count); + + 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, |terminal, cx| { + let (point, side) = TerminalEl::mouse_to_cell_data( + position, + origin, + cur_size, + terminal.get_display_offset(), + ); + + terminal.drag(point, side); + + cx.notify() + }); + } + }, + ), + ); + } + + ///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 { + type LayoutState = LayoutState; + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + cx: &mut gpui::LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + 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(dimensions.clone()), |content, cursor_text| { + let (cells, rects, highlights) = TerminalEl::layout_grid( + content.display_iter, + &text_style, + terminal_theme, + cx.text_layout_cache, + self.modal, + content.selection, + ); + + //Layout cursor + 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 { + terminal_theme.colors.modal_background + } else { + terminal_theme.colors.background + }; + + //Done! + ( + constraint.max, + LayoutState { + cells, + cursor, + background_color, + selection_color, + size: dimensions, + rects, + highlights, + }, + ) + } + + fn paint( + &mut self, + bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + //Setup element stuff + let clip_bounds = Some(visible_bounds); + + cx.paint_layer(clip_bounds, |cx| { + 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.size, cx); + + cx.paint_layer(clip_bounds, |cx| { + //Start with a background color + cx.scene.push_quad(Quad { + bounds: RectF::new(bounds.origin(), bounds.size()), + background: Some(layout.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 start_y = layout.highlights.get(0).map(|highlight| { + origin.y() + highlight.line_index as f32 * layout.size.line_height + }); + + if let Some(y) = start_y { + let range_lines = layout + .highlights + .iter() + .map(|relative_highlight| { + relative_highlight.to_highlighted_range_line(origin, layout) + }) + .collect::>(); + + let hr = HighlightedRange { + start_y: y, //Need to change this + 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.size.line_height, + }; + hr.paint(bounds, cx.scene); + } + }); + + //Draw the text cells + cx.paint_layer(clip_bounds, |cx| { + for cell in &layout.cells { + cell.paint(origin, layout, visible_bounds, cx); + } + }); + + //Draw cursor + if let Some(cursor) = &layout.cursor { + cx.paint_layer(clip_bounds, |cx| { + cursor.paint(origin, cx); + }) + } + }); + } + + fn dispatch_event( + &mut self, + event: &gpui::Event, + _bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + _paint: &mut Self::PaintState, + cx: &mut gpui::EventContext, + ) -> bool { + match event { + Event::ScrollWheel(ScrollWheelEvent { + delta, position, .. + }) => visible_bounds + .contains_point(*position) + .then(|| { + let vertical_scroll = + (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER; + + self.terminal.upgrade(cx.app).map(|terminal| { + terminal + .read(cx.app) + .scroll(Scroll::Delta(vertical_scroll.round() as i32)); + }); + }) + .is_some(), + Event::KeyDown(KeyDownEvent { keystroke, .. }) => { + if !cx.is_parent_view_focused() { + 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.terminal + .upgrade(cx.app) + .map(|model_handle| model_handle.read(cx.app)) + .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, + _layout: &Self::LayoutState, + _paint: &Self::PaintState, + _cx: &gpui::DebugContext, + ) -> gpui::serde_json::Value { + json!({ + "type": "TerminalElement", + }) + } +} + +mod test { + + #[test] + fn test_mouse_to_selection() { + 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 = crate::connected_el::TermDimensions::new( + line_height, + cell_width, + gpui::geometry::vector::vec2f(term_width, term_height), + ); + + 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::connected_el::TerminalEl::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/connected_view.rs b/crates/terminal/src/connected_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..633bd70b5a2f4845331c01ff8437e14621503a8a --- /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/connection.rs b/crates/terminal/src/connection.rs deleted file mode 100644 index 0e051da17caad14e284d45831e0d5ddc86460530..0000000000000000000000000000000000000000 --- a/crates/terminal/src/connection.rs +++ /dev/null @@ -1,252 +0,0 @@ -mod keymappings; - -use alacritty_terminal::{ - ansi::{ClearMode, Handler}, - config::{Config, Program, PtyConfig}, - event::{Event as AlacTermEvent, Notify}, - event_loop::{EventLoop, Msg, Notifier}, - grid::Scroll, - sync::FairMutex, - term::{SizeInfo, TermMode}, - tty::{self, setup_env}, - Term, -}; -use futures::{channel::mpsc::unbounded, 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 self::keymappings::to_esc_str; - -const DEFAULT_TITLE: &str = "Terminal"; - -///Upward flowing events, for changing the title and such -#[derive(Copy, Clone, Debug)] -pub enum Event { - TitleChanged, - CloseTerminal, - Activate, - Wakeup, - Bell, -} - -pub struct TerminalConnection { - pub pty_tx: Notifier, - pub term: Arc>>, - pub title: String, - pub associated_directory: Option, -} - -impl TerminalConnection { - pub fn new( - working_directory: Option, - shell: Option, - env_vars: Option>, - initial_size: SizeInfo, - cx: &mut ModelContext, - ) -> TerminalConnection { - let pty_config = { - let shell = shell.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, - working_directory: working_directory.clone(), - hold: false, - } - }; - - let mut env: HashMap = HashMap::new(); - if let Some(envs) = env_vars { - for (var, val) in envs { - env.insert(var, val); - } - } - - //TODO: Properly set the current locale, - env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); - - let config = Config { - pty_config: pty_config.clone(), - env, - ..Default::default() - }; - - 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(); - - //Set up the terminal... - let term = Term::new(&config, initial_size, ZedListener(events_tx.clone())); - 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 :(") - } - }; - - //And connect them together - let event_loop = EventLoop::new( - term.clone(), - ZedListener(events_tx.clone()), - pty, - pty_config.hold, - false, - ); - - //Kick things off - let pty_tx = event_loop.channel(); - let _io_thread = event_loop.spawn(); - - 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); - cx.notify(); - }); - } - None => break, - } - } - }) - .detach(); - - TerminalConnection { - pty_tx: Notifier(pty_tx), - term, - title: DEFAULT_TITLE.to_string(), - associated_directory: working_directory, - } - } - - ///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, - ) { - match event { - // TODO: Handle is_self_focused in subscription on terminal view - AlacTermEvent::Wakeup => { - cx.emit(Event::Wakeup); - } - AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), - AlacTermEvent::MouseCursorDirty => { - //Calculate new cursor style. - //TODO: alacritty/src/input.rs:L922-L939 - //Check on correctly handling mouse events for terminals - cx.platform().set_cursor_style(CursorStyle::Arrow); //??? - } - AlacTermEvent::Title(title) => { - self.title = title; - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ResetTitle => { - self.title = DEFAULT_TITLE.to_string(); - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ClipboardStore(_, data) => { - cx.write_to_clipboard(ClipboardItem::new(data)) - } - AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format( - &cx.read_from_clipboard() - .map(|ci| ci.text().to_string()) - .unwrap_or("".to_string()), - )), - AlacTermEvent::ColorRequest(index, format) => { - let color = self.term.lock().colors()[index].unwrap_or_else(|| { - let term_style = &cx.global::().theme.terminal; - to_alac_rgb(get_color_at_index(&index, &term_style.colors)) - }); - self.write_to_pty(format(color)) - } - AlacTermEvent::CursorBlinkingChange => { - //TODO: Set a timer to blink the cursor on and off - } - AlacTermEvent::Bell => { - cx.emit(Event::Bell); - } - AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), - } - } - - ///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) { - 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) { - self.term.lock().scroll_display(Scroll::Bottom); - self.pty_tx.notify(input); - } - - ///Resize the terminal and the PTY. This locks the terminal. - pub fn set_size(&mut self, new_size: SizeInfo) { - self.pty_tx.0.send(Msg::Resize(new_size)).ok(); - self.term.lock().resize(new_size); - } - - pub fn clear(&mut self) { - self.write_to_pty("\x0c".into()); - self.term.lock().clear_screen(ClearMode::Saved); - } - - pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool { - let guard = self.term.lock(); - let mode = guard.mode(); - let esc = to_esc_str(keystroke, mode); - drop(guard); - if esc.is_some() { - self.write_to_pty(esc.unwrap()); - true - } else { - false - } - } - - ///Paste text into the terminal - pub fn paste(&mut 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()); - self.write_to_pty("\x1b[201~".to_string()); - } else { - self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); - } - } -} - -impl Drop for TerminalConnection { - fn drop(&mut self) { - self.pty_tx.0.send(Msg::Shutdown).ok(); - } -} - -impl Entity for TerminalConnection { - type Event = Event; -} 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 946a22d3043e3eb1238b833974a17b29143e7ddf..1a425ebaeddd252034d1c78e703336dc01915874 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 a4d429843bf17aa51760b69e3b00f8317b681d15..51d02d6bb2f00a21d0450f823de3fa193a33474c 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 0000000000000000000000000000000000000000..cde6c337eace0f35aeea83fc9e71cc0c98ab3807 --- /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.rs deleted file mode 100644 index 708f96856b03522d5dabab61cae178a340c9db27..0000000000000000000000000000000000000000 --- a/crates/terminal/src/modal.rs +++ /dev/null @@ -1,63 +0,0 @@ -use gpui::{ModelHandle, ViewContext}; -use workspace::Workspace; - -use crate::{get_wd_for_workspace, DeployModal, Event, Terminal, TerminalConnection}; - -#[derive(Debug)] -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 - let possible_connection = - cx.update_default_global::, _, _>(|possible_connection, _| { - possible_connection.take() - }); - - 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.set_global::>(Some(StoredConnection( - stored_connection.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); - let this = cx.add_view(|cx| Terminal::new(wd, true, cx)); - 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 - cx.set_global::>(Some(StoredConnection( - connection_handle.clone(), - ))); - this - }) { - let connection = closed_terminal_handle.read(cx).connection.clone(); - cx.set_global(Some(StoredConnection(connection))); - } - } - - //The problem is that the terminal modal is never re-stored. -} - -pub fn on_event( - workspace: &mut Workspace, - _: ModelHandle, - event: &Event, - cx: &mut ViewContext, -) { - // 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() - { - workspace.dismiss_modal(cx) - } - } -} diff --git a/crates/terminal/src/modal_view.rs b/crates/terminal/src/modal_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec5280befc7a767777010da8937397b0cff5eb51 --- /dev/null +++ b/crates/terminal/src/modal_view.rs @@ -0,0 +1,73 @@ +use gpui::{ModelHandle, ViewContext}; +use workspace::Workspace; + +use crate::{ + get_working_directory, model::Terminal, DeployModal, Event, TerminalContent, TerminalView, +}; + +#[derive(Debug)] +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_terminal = + cx.update_default_global::, _, _>(|possible_connection, _| { + possible_connection.take() + }); + + if let Some(StoredTerminal(stored_terminal)) = possible_terminal { + workspace.toggle_modal(cx, |_, 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)) + }); + // 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| { + // No terminal modal visible, construct a new one. + let working_directory = get_working_directory(workspace, cx); + + 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).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 + cx.set_global::>(Some(StoredTerminal( + terminal_handle.clone(), + ))); + } + + this + }) { + // 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).handle(); + // 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(), + ))); + } + } + } +} + +pub fn on_event( + workspace: &mut Workspace, + _: ModelHandle, + event: &Event, + cx: &mut ViewContext, +) { + // Dismiss the modal if the terminal quit + if let Event::CloseTerminal = event { + cx.set_global::>(None); + if workspace.modal::().is_some() { + workspace.dismiss_modal(cx) + } + } +} diff --git a/crates/terminal/src/model.rs b/crates/terminal/src/model.rs new file mode 100644 index 0000000000000000000000000000000000000000..f1b2dd36cff0d698131cbf70b5ed82f8e19e4f1e --- /dev/null +++ b/crates/terminal/src/model.rs @@ -0,0 +1,522 @@ +use alacritty_terminal::{ + ansi::{ClearMode, Handler}, + config::{Config, Program, PtyConfig}, + event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, + event_loop::{EventLoop, Msg, Notifier}, + grid::Scroll, + index::{Direction, Point}, + selection::{Selection, SelectionType}, + sync::FairMutex, + term::{test::TermSize, RenderableContent, TermMode}, + tty::{self, setup_env}, + Term, +}; +use anyhow::{bail, Result}; +use futures::{ + channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, + StreamExt, +}; +use settings::{Settings, Shell}; +use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc}; +use thiserror::Error; + +use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; + +use crate::{ + connected_el::TermDimensions, + mappings::{ + colors::{get_color_at_index, to_alac_rgb}, + keys::to_esc_str, + }, +}; + +const DEFAULT_TITLE: &str = "Terminal"; + +///Upward flowing events, for changing the title and such +#[derive(Copy, Clone, Debug)] +pub enum Event { + TitleChanged, + CloseTerminal, + 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(); + } +} + +#[derive(Error, Debug)] +pub struct TerminalError { + pub directory: Option, + pub shell: Option, + pub source: std::io::Error, +} + +impl TerminalError { + pub fn fmt_directory(&self) -> String { + self.directory + .clone() + .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().to_string()); + match default_dir { + Some(dir) => format!(" {}", dir), + None => "".to_string(), + } + }) + } + + 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 => { + 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(), + } + }) + } +} + +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, + "Working directory: {} Shell command: `{}`, IOError: {}", + dir_string, shell, self.source + ) + } +} + +pub struct TerminalBuilder { + terminal: Terminal, + events_rx: UnboundedReceiver, +} + +impl TerminalBuilder { + pub fn new( + working_directory: Option, + shell: Option, + env: Option>, + initial_size: TermDimensions, + ) -> Result { + let pty_config = { + 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: alac_shell, + working_directory: working_directory.clone(), + hold: false, + } + }; + + 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()); + + let config = Config { + pty_config: pty_config.clone(), + env, + ..Default::default() + }; + + setup_env(&config); + + //Spawn a task so the Alacritty EventLoop can communicate with us in a view context + let (events_tx, events_rx) = unbounded(); + + //Set up the terminal... + 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.into(), None) { + Ok(pty) => pty, + Err(error) => { + bail!(TerminalError { + directory: working_directory, + shell, + source: error, + }); + } + }; + + let shell_txt = { + 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 + let event_loop = EventLoop::new( + term.clone(), + ZedListener(events_tx.clone()), + pty, + pty_config.hold, + false, + ); + + //Kick things off + let pty_tx = event_loop.channel(); + let _io_thread = event_loop.spawn(); + + let terminal = Terminal { + pty_tx: Notifier(pty_tx), + term, + title: shell_txt.to_string(), + }; + + Ok(TerminalBuilder { + terminal, + events_rx, + }) + } + + 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 { + match this.upgrade(&cx) { + Some(this) => { + this.update(&mut cx, |this, cx| { + this.process_terminal_event(event, cx); + + cx.notify(); + }); + } + None => break, + } + } + }) + .detach(); + + self.terminal + } +} + +pub struct Terminal { + pty_tx: Notifier, + term: Arc>>, + pub title: String, +} + +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, + ) { + match event { + // TODO: Handle is_self_focused in subscription on terminal view + AlacTermEvent::Wakeup => { + cx.emit(Event::Wakeup); + } + AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), + AlacTermEvent::MouseCursorDirty => { + //Calculate new cursor style. + //TODO: alacritty/src/input.rs:L922-L939 + //Check on correctly handling mouse events for terminals + cx.platform().set_cursor_style(CursorStyle::Arrow); //??? + } + AlacTermEvent::Title(title) => { + self.title = title; + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ResetTitle => { + self.title = DEFAULT_TITLE.to_string(); + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ClipboardStore(_, data) => { + cx.write_to_clipboard(ClipboardItem::new(data)) + } + AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format( + &cx.read_from_clipboard() + .map(|ci| ci.text().to_string()) + .unwrap_or("".to_string()), + )), + AlacTermEvent::ColorRequest(index, format) => { + let color = self.term.lock().colors()[index].unwrap_or_else(|| { + let term_style = &cx.global::().theme.terminal; + to_alac_rgb(get_color_at_index(&index, &term_style.colors)) + }); + self.write_to_pty(format(color)) + } + AlacTermEvent::CursorBlinkingChange => { + //TODO: Set a timer to blink the cursor on and off + } + AlacTermEvent::Bell => { + cx.emit(Event::Bell); + } + AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), + AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"), + } + } + + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. + 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(&self, input: Vec) { + self.term.lock().scroll_display(Scroll::Bottom); + self.pty_tx.notify(input); + } + + ///Resize the terminal and the PTY. This locks the terminal. + pub fn set_size(&self, new_size: WindowSize) { + self.pty_tx.0.send(Msg::Resize(new_size)).ok(); + + 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) { + self.write_to_pty("\x0c".into()); + self.term.lock().clear_screen(ClearMode::Saved); + } + + pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool { + let guard = self.term.lock(); + let mode = guard.mode(); + let esc = to_esc_str(keystroke, mode); + drop(guard); + if esc.is_some() { + self.write_to_pty(esc.unwrap()); + true + } else { + false + } + } + + ///Paste text into the terminal + 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()); + self.write_to_pty("\x1b[201~".to_string()); + } else { + self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); + } + } + + 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; + } + + pub fn render_lock(&self, new_size: Option, f: F) -> T + where + 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 + //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(); + let cursor_text = term.grid()[content.cursor.point].c; + + f(content, cursor_text) + } + + pub fn get_display_offset(&self) -> usize { + self.term.lock().renderable_content().display_offset + } + + ///Scroll the terminal + pub fn scroll(&self, scroll: Scroll) { + self.term.lock().scroll_display(scroll) + } + + 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, + }; + + let selection = + selection_type.map(|selection_type| Selection::new(selection_type, point, side)); + + 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 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}; + use libc; + 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())) + } +} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9a28f82c2d1c51e75f8ef17fc84bd1fc006cfc77..2f5ef5ffab3dc65499b73d34acef1bb684f5d4d0 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,261 +1,168 @@ -mod color_translation; -pub mod connection; -mod modal; -pub mod terminal_element; - -use alacritty_terminal::{ - event::{Event as AlacTermEvent, EventListener}, - term::SizeInfo, -}; +pub mod connected_el; +pub mod connected_view; +pub mod mappings; +pub mod modal_view; +pub mod model; -use connection::{Event, TerminalConnection}; +use connected_view::ConnectedView; 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, + 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 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.; -//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); - -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(Terminal::ctrl_c); - cx.add_action(Terminal::up); - cx.add_action(Terminal::down); - cx.add_action(Terminal::escape); - cx.add_action(Terminal::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::input); - cx.add_action(Terminal::clear); + + connected_view::init(cx); } -///A translation struct for Alacritty to communicate with us from their event loop -#[derive(Clone)] -pub struct ZedListener(UnboundedSender); +//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 EventListener for ZedListener { - fn send_event(&self, event: AlacTermEvent) { - self.0.unbounded_send(event).ok(); +impl TerminalContent { + fn handle(&self) -> AnyViewHandle { + match self { + Self::Connected(handle) => handle.into(), + Self::Error(handle) => handle.into(), + } } } -///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct Terminal { - connection: 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 +pub struct TerminalView { modal: bool, + content: TerminalContent, + associated_directory: Option, +} + +pub struct ErrorView { + error: TerminalError, +} + +impl Entity for TerminalView { + type Event = Event; +} + +impl Entity for ConnectedView { + type Event = Event; } -impl Entity for Terminal { +impl Entity for ErrorView { type Event = Event; } -impl Terminal { +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 = SizeInfo::new( - DEBUG_TERMINAL_WIDTH, - DEBUG_TERMINAL_HEIGHT, - DEBUG_CELL_WIDTH, + let size_info = TermDimensions::new( DEBUG_LINE_HEIGHT, - 0., - 0., - false, + DEBUG_CELL_WIDTH, + 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 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)); + 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()); - let connection = cx - .add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx)); - - Terminal::from_connection(connection, modal, cx) + TerminalView { + modal, + content, + associated_directory: working_directory, + } } - fn from_connection( - connection: ModelHandle, + fn from_terminal( + terminal: ModelHandle, modal: bool, cx: &mut ViewContext, - ) -> Terminal { - 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); - } - } - Event::Bell => { - this.has_bell = true; - cx.emit(Event::TitleChanged); - } - _ => cx.emit(*event), - }) - .detach(); - - Terminal { - connection, - has_new_content: true, - has_bell: false, + ) -> Self { + let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); + TerminalView { modal, + content: TerminalContent::Connected(connected_view), + associated_directory: None, } } - - 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(&mut self, _: &Clear, cx: &mut ViewContext) { - self.connection - .update(cx, |connection, _| connection.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); - workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(wd, false, cx))), 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 => (), - } - } - - ///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()); - }) - } - } - - ///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()); - }); - } - - ///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()); - }); - } - - ///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()); - }); - } - - ///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()); - }); - } - - ///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()); - }); - } } -impl View for Terminal { +impl View for TerminalView { fn ui_name() -> &'static str { "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() + 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 { @@ -267,52 +174,81 @@ impl View for Terminal { } } -impl Item for Terminal { +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 = TerminalEl::make_text_style(cx.font_cache(), settings); + + //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() + } +} + +impl Item for TerminalView { fn tab_content( &self, _detail: Option, 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(); + let title = match &self.content { + TerminalContent::Connected(connected) => { + connected.read(cx).handle().read(cx).title.clone() + } + TerminalContent::Error(_) => "Terminal".to_string(), + }; - 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) + Flex::row() + .with_child( + Label::new(title, tab_theme.label.clone()) .aligned() + .contained() .boxed(), - ); - }; - - flex.with_child( - Label::new( - self.connection.read(cx).title.clone(), - tab_theme.label.clone(), ) - .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 { //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( - self.connection.read(cx).associated_directory.clone(), + Some(TerminalView::new( + self.associated_directory.clone(), false, cx, )) @@ -361,8 +297,20 @@ impl Item for Terminal { 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, 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 { @@ -379,7 +327,7 @@ impl Item for Terminal { } ///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 @@ -390,10 +338,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()) } @@ -438,7 +388,6 @@ mod tests { use gpui::TestAppContext; use std::path::Path; - use workspace::AppState; mod terminal_test_context; @@ -446,7 +395,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; @@ -458,12 +407,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(); @@ -482,28 +429,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(); - //Test - cx.read(|cx| { + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + cx.create_file_wt(project.clone(), "/root.txt").await; + + cx.cx.read(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -522,27 +453,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(); @@ -560,53 +476,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(); @@ -623,51 +500,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/terminal_element.rs b/crates/terminal/src/terminal_element.rs deleted file mode 100644 index f1ba70cab92089aedf1ef82615f16a2b49c1bd16..0000000000000000000000000000000000000000 --- a/crates/terminal/src/terminal_element.rs +++ /dev/null @@ -1,807 +0,0 @@ -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, -}; -use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; -use gpui::{ - color::Color, - elements::*, - fonts::{TextStyle, Underline}, - geometry::{ - rect::RectF, - vector::{vec2f, Vector2F}, - }, - json::json, - text_layout::{Line, RunStyle}, - Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, - PaintContext, Quad, ScrollWheelEvent, SizeConstraint, TextLayoutCache, WeakModelHandle, -}; -use itertools::Itertools; -use ordered_float::OrderedFloat; -use settings::Settings; -use theme::TerminalStyle; -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}; - -///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 { - connection: WeakModelHandle, - view_id: usize, - modal: bool, -} - -///New type pattern so I don't mix these two up -struct CellWidth(f32); -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); - -///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 -} - -#[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, - line_height: LineHeight, - em_width: CellWidth, - cursor: Option, - background_color: Color, - cur_size: SizeInfo, - terminal: Arc>>, - selection_color: Color, -} - -impl TerminalEl { - pub fn new( - view_id: usize, - connection: WeakModelHandle, - modal: bool, - ) -> TerminalEl { - TerminalEl { - view_id, - connection, - modal, - } - } -} - -impl Element for TerminalEl { - type LayoutState = LayoutState; - type PaintState = (); - - fn layout( - &mut self, - 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(); - - //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 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(); - - let layout_lines = layout_lines( - content.display_iter, - &text_style, - 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(), - }, - )], - ); - - 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); - - let background_color = if self.modal { - terminal_theme.colors.modal_background - } else { - terminal_theme.colors.background - }; - - ( - constraint.max, - LayoutState { - layout_lines, - line_height, - em_width: cell_width, - cursor, - cur_size, - background_color, - terminal: terminal_mutex, - selection_color, - }, - ) - } - - fn paint( - &mut self, - bounds: gpui::geometry::rect::RectF, - visible_bounds: gpui::geometry::rect::RectF, - layout: &mut Self::LayoutState, - cx: &mut gpui::PaintContext, - ) -> Self::PaintState { - //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, - ); - - cx.paint_layer(clip_bounds, |cx| { - //Start with a background color - cx.scene.push_quad(Quad { - bounds: RectF::new(bounds.origin(), bounds.size()), - background: Some(layout.background_color), - border: Default::default(), - corner_radius: 0., - }); - - //Draw cell backgrounds - for layout_line in &layout.layout_lines { - 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., - }) - } - } - }); - - //Draw Selection - cx.paint_layer(clip_bounds, |cx| { - let mut highlight_y = None; - let highlight_lines = layout - .layout_lines - .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; - - return Some(HighlightedRangeLine { start_x, end_x }); - } else { - return None; - } - }) - .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, - color: layout.selection_color, - //Copied from editor. TODO: move to theme or something - corner_radius: 0.15 * layout.line_height.0, - }; - hr.paint(bounds, cx.scene); - } - }); - - cx.paint_layer(clip_bounds, |cx| { - for layout_line in &layout.layout_lines { - 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, - ); - } - } - }); - - //Draw cursor - if let Some(cursor) = &layout.cursor { - cx.paint_layer(clip_bounds, |cx| { - cursor.paint(origin, cx); - }) - } - - #[cfg(debug_assertions)] - if DEBUG_GRID { - cx.paint_layer(clip_bounds, |cx| { - draw_debug_grid(bounds, layout, cx); - }) - } - }); - } - - fn dispatch_event( - &mut self, - event: &gpui::Event, - _bounds: gpui::geometry::rect::RectF, - visible_bounds: gpui::geometry::rect::RectF, - layout: &mut Self::LayoutState, - _paint: &mut Self::PaintState, - cx: &mut gpui::EventContext, - ) -> bool { - match event { - Event::ScrollWheel(ScrollWheelEvent { - delta, position, .. - }) => visible_bounds - .contains_point(*position) - .then(|| { - 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)); - }) - } - }) - .is_some(), - Event::KeyDown(KeyDownEvent { keystroke, .. }) => { - if !cx.is_parent_view_focused() { - return false; - } - - self.connection - .upgrade(cx.app) - .map(|connection| { - connection - .update(cx.app, |connection, _| connection.try_keystroke(keystroke)) - }) - .unwrap_or(false) - } - _ => false, - } - } - - fn debug( - &self, - _bounds: gpui::geometry::rect::RectF, - _layout: &Self::LayoutState, - _paint: &Self::PaintState, - _cx: &gpui::DebugContext, - ) -> gpui::serde_json::Value { - json!({ - "type": "TerminalElement", - }) - } -} - -pub fn mouse_to_cell_data( - pos: Vector2F, - origin: Vector2F, - cur_size: SizeInfo, - 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) -} - -///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, - 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, - ) -} - -fn layout_lines( - 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); - } - - let cell_text = &indexed_cell.c.to_string(); - - let cell_style = cell_style(&indexed_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), - layout_cell, - convert_color(&indexed_cell.bg, &terminal_theme.colors, modal), - ) - }) - .collect::>(); - - LayoutLine { - cells, - highlighted_range, - } - }) - .collect::>() -} - -// 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: &LineHeight, - cell_width: &CellWidth, - 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 - } else { - text_fragment.width() - }; - - Some(( - vec2f( - line_index as f32 * cell_width.0, - cursor_line as f32 * line_height.0, - ), - 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( - 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) -} - -///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] - fn test_mouse_to_selection() { - 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), - ) - ); - } - - #[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 b5696aff1387811356e3f6ca65c557558b621d46..e78939224b3c5c8011c121cf9f2a529830e770fa 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -1,35 +1,40 @@ -use std::time::Duration; +use std::{path::Path, time::Duration}; -use alacritty_terminal::term::SizeInfo; -use gpui::{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::TerminalConnection, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, - DEBUG_TERMINAL_WIDTH, + connected_el::TermDimensions, + model::{Terminal, TerminalBuilder}, + 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: 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 = SizeInfo::new( - DEBUG_TERMINAL_WIDTH, - DEBUG_TERMINAL_HEIGHT, + let size_info = TermDimensions::new( DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, - 0., - 0., - false, + vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), ); - let connection = - cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx)); + let connection = term.then(|| { + cx.add_model(|cx| { + TerminalBuilder::new(None, None, None, size_info) + .unwrap() + .subscribe(cx) + }) + }); TerminalTestContext { cx, connection } } @@ -38,34 +43,112 @@ 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: &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); - 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") + }) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 587ad09cb5a4ca8c11f2042100a8315ab729a0d6..9b19ec2a106d42b3548a7660a1092aa701c10aef 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) { diff --git a/styles/package-lock.json b/styles/package-lock.json index 582f1c84968a5c1a25ddac5fd3c21ba907353c6d..5499f1852cb4330467268dee6436b53589a90e9b 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": {