Merge branch 'main' into ime-support-2

Antonio Scandurra created

Change summary

Cargo.lock                                         |  13 
assets/keymaps/default.json                        |   2 
assets/settings/default.json                       |   8 
crates/command_palette/src/command_palette.rs      |  14 
crates/diagnostics/src/items.rs                    |   8 
crates/editor/src/editor.rs                        |  30 
crates/editor/src/element.rs                       | 152 +
crates/editor/src/hover_popover.rs                 | 368 +++++-
crates/editor/src/test.rs                          |  78 +
crates/file_finder/src/file_finder.rs              |  10 
crates/language/src/buffer.rs                      |   6 
crates/settings/src/settings.rs                    |   2 
crates/terminal/Cargo.toml                         |   6 
crates/terminal/src/connected_el.rs                | 853 ++++++++++++++++
crates/terminal/src/connected_view.rs              | 176 +++
crates/terminal/src/connection.rs                  | 252 ----
crates/terminal/src/mappings/colors.rs             |   2 
crates/terminal/src/mappings/keys.rs               |  23 
crates/terminal/src/mappings/mod.rs                |   2 
crates/terminal/src/modal.rs                       |  63 -
crates/terminal/src/modal_view.rs                  |  73 +
crates/terminal/src/model.rs                       | 522 +++++++++
crates/terminal/src/terminal.rs                    | 573 +++------
crates/terminal/src/terminal_element.rs            | 828 ---------------
crates/terminal/src/tests/terminal_test_context.rs | 139 ++
crates/text/src/selection.rs                       |  14 
crates/theme/src/theme.rs                          |   3 
crates/workspace/src/workspace.rs                  |   6 
crates/zed/src/menus.rs                            |   2 
styles/src/styleTree/hoverPopover.ts               |  50 
styles/src/themes/common/base16.ts                 |  23 
styles/src/themes/common/theme.ts                  |   3 
32 files changed, 2,517 insertions(+), 1,787 deletions(-)

Detailed changes

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",
 ]

assets/keymaps/default.json 🔗

@@ -188,7 +188,7 @@
             "alt-down": "editor::SelectSmallerSyntaxNode",
             "cmd-u": "editor::UndoSelection",
             "cmd-shift-u": "editor::RedoSelection",
-            "f8": "editor::GoToNextDiagnostic",
+            "f8": "editor::GoToDiagnostic",
             "shift-f8": "editor::GoToPrevDiagnostic",
             "f2": "editor::Rename",
             "f12": "editor::GoToDefinition",

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"

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::<CommandPalette>()
-                .unwrap()
+            workspace.modal::<CommandPalette>().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::<CommandPalette>()
-                .unwrap()
+            workspace.modal::<CommandPalette>().unwrap()
         });
 
         palette

crates/diagnostics/src/items.rs 🔗

@@ -1,5 +1,5 @@
 use collections::HashSet;
-use editor::{Editor, GoToNextDiagnostic};
+use editor::{Editor, GoToDiagnostic};
 use gpui::{
     elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MouseButton,
     MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
@@ -48,10 +48,10 @@ impl DiagnosticIndicator {
         }
     }
 
-    fn go_to_next_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext<Self>) {
+    fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
         if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade(cx)) {
             editor.update(cx, |editor, cx| {
-                editor.go_to_diagnostic(editor::Direction::Next, cx);
+                editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
             })
         }
     }
@@ -202,7 +202,7 @@ impl View for DiagnosticIndicator {
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, |_, cx| {
-                    cx.dispatch_action(GoToNextDiagnostic)
+                    cx.dispatch_action(GoToDiagnostic)
                 })
                 .boxed(),
             );

crates/editor/src/editor.rs 🔗

@@ -82,9 +82,6 @@ pub struct SelectNext {
     pub replace_newest: bool,
 }
 
-#[derive(Clone, PartialEq)]
-pub struct GoToDiagnostic(pub Direction);
-
 #[derive(Clone, PartialEq)]
 pub struct Scroll(pub Vector2F);
 
@@ -135,7 +132,7 @@ actions!(
         Backspace,
         Delete,
         Newline,
-        GoToNextDiagnostic,
+        GoToDiagnostic,
         GoToPrevDiagnostic,
         Indent,
         Outdent,
@@ -297,7 +294,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::move_to_enclosing_bracket);
     cx.add_action(Editor::undo_selection);
     cx.add_action(Editor::redo_selection);
-    cx.add_action(Editor::go_to_next_diagnostic);
+    cx.add_action(Editor::go_to_diagnostic);
     cx.add_action(Editor::go_to_prev_diagnostic);
     cx.add_action(Editor::go_to_definition);
     cx.add_action(Editor::page_up);
@@ -4567,17 +4564,32 @@ impl Editor {
         self.selection_history.mode = SelectionHistoryMode::Normal;
     }
 
-    fn go_to_next_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext<Self>) {
-        self.go_to_diagnostic(Direction::Next, cx)
+    fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
+        self.go_to_diagnostic_impl(Direction::Next, cx)
     }
 
     fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext<Self>) {
-        self.go_to_diagnostic(Direction::Prev, cx)
+        self.go_to_diagnostic_impl(Direction::Prev, cx)
     }
 
-    pub fn go_to_diagnostic(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+    pub fn go_to_diagnostic_impl(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         let buffer = self.buffer.read(cx).snapshot(cx);
         let selection = self.selections.newest::<usize>(cx);
+
+        // If there is an active Diagnostic Popover. Jump to it's diagnostic instead.
+        if direction == Direction::Next {
+            if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
+                let (group_id, jump_to) = popover.activation_info();
+                self.activate_diagnostics(group_id, cx);
+                self.change_selections(Some(Autoscroll::Center), cx, |s| {
+                    let mut new_selection = s.newest_anchor().clone();
+                    new_selection.collapse_to(jump_to, SelectionGoal::None);
+                    s.select_anchors(vec![new_selection.clone()]);
+                });
+                return;
+            }
+        }
+
         let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
             active_diagnostics
                 .primary_range

crates/editor/src/element.rs 🔗

@@ -41,6 +41,10 @@ use std::{
     ops::Range,
 };
 
+const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
+const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
+const HOVER_POPOVER_GAP: f32 = 10.;
+
 struct SelectionLayout {
     head: DisplayPoint,
     range: Range<DisplayPoint>,
@@ -268,8 +272,9 @@ impl EditorElement {
         }
 
         if paint
-            .hover_bounds
-            .map_or(false, |hover_bounds| hover_bounds.contains_point(position))
+            .hover_popover_bounds
+            .iter()
+            .any(|hover_bounds| hover_bounds.contains_point(position))
         {
             return false;
         }
@@ -585,34 +590,77 @@ impl EditorElement {
             cx.scene.pop_stacking_context();
         }
 
-        if let Some((position, hover_popover)) = layout.hover.as_mut() {
+        if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
             cx.scene.push_stacking_context(None);
 
             // This is safe because we check on layout whether the required row is available
             let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
-            let size = hover_popover.size();
+
+            // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
+            // height. This is the size we will use to decide whether to render popovers above or below
+            // the hovered line.
+            let first_size = hover_popovers[0].size();
+            let height_to_reserve =
+                first_size.y() + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.line_height;
+
+            // Compute Hovered Point
             let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
-            let y = position.row() as f32 * layout.line_height - scroll_top - size.y();
-            let mut popover_origin = content_origin + vec2f(x, y);
+            let y = position.row() as f32 * layout.line_height - scroll_top;
+            let hovered_point = content_origin + vec2f(x, y);
 
-            if popover_origin.y() < 0.0 {
-                popover_origin.set_y(popover_origin.y() + layout.line_height + size.y());
-            }
+            paint.hover_popover_bounds.clear();
 
-            let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
-            if x_out_of_bounds < 0.0 {
-                popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
-            }
+            if hovered_point.y() - height_to_reserve > 0.0 {
+                // There is enough space above. Render popovers above the hovered point
+                let mut current_y = hovered_point.y();
+                for hover_popover in hover_popovers {
+                    let size = hover_popover.size();
+                    let mut popover_origin = vec2f(hovered_point.x(), current_y - size.y());
 
-            hover_popover.paint(
-                popover_origin,
-                RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
-                cx,
-            );
+                    let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
+                    if x_out_of_bounds < 0.0 {
+                        popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
+                    }
 
-            paint.hover_bounds = Some(
-                RectF::new(popover_origin, hover_popover.size()).dilate(Vector2F::new(0., 5.)),
-            );
+                    hover_popover.paint(
+                        popover_origin,
+                        RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
+                        cx,
+                    );
+
+                    paint.hover_popover_bounds.push(
+                        RectF::new(popover_origin, hover_popover.size())
+                            .dilate(Vector2F::new(0., 5.)),
+                    );
+
+                    current_y = popover_origin.y() - HOVER_POPOVER_GAP;
+                }
+            } else {
+                // There is not enough space above. Render popovers below the hovered point
+                let mut current_y = hovered_point.y() + layout.line_height;
+                for hover_popover in hover_popovers {
+                    let size = hover_popover.size();
+                    let mut popover_origin = vec2f(hovered_point.x(), current_y);
+
+                    let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
+                    if x_out_of_bounds < 0.0 {
+                        popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
+                    }
+
+                    hover_popover.paint(
+                        popover_origin,
+                        RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
+                        cx,
+                    );
+
+                    paint.hover_popover_bounds.push(
+                        RectF::new(popover_origin, hover_popover.size())
+                            .dilate(Vector2F::new(0., 5.)),
+                    );
+
+                    current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP;
+                }
+            }
 
             cx.scene.pop_stacking_context();
         }
@@ -1147,6 +1195,8 @@ impl Element for EditorElement {
         });
 
         let scroll_position = snapshot.scroll_position();
+        // The scroll position is a fractional point, the whole number of which represents
+        // the top of the window in terms of display rows.
         let start_row = scroll_position.y() as u32;
         let scroll_top = scroll_position.y() * line_height;
 
@@ -1320,16 +1370,8 @@ impl Element for EditorElement {
                     .map(|indicator| (newest_selection_head.row(), indicator));
             }
 
-            hover = view.hover_state.popover.clone().and_then(|hover| {
-                let (point, rendered) = hover.render(&snapshot, style.clone(), cx);
-                if point.row() >= snapshot.scroll_position().y() as u32 {
-                    if line_layouts.len() > (point.row() - start_row) as usize {
-                        return Some((point, rendered));
-                    }
-                }
-
-                None
-            });
+            let visible_rows = start_row..start_row + line_layouts.len() as u32;
+            hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
         });
 
         if let Some((_, context_menu)) = context_menu.as_mut() {
@@ -1352,21 +1394,23 @@ impl Element for EditorElement {
             );
         }
 
-        if let Some((_, hover)) = hover.as_mut() {
-            hover.layout(
-                SizeConstraint {
-                    min: Vector2F::zero(),
-                    max: vec2f(
-                        (120. * em_width) // Default size
-                            .min(size.x() / 2.) // Shrink to half of the editor width
-                            .max(20. * em_width), // Apply minimum width of 20 characters
-                        (16. * line_height) // Default size
-                            .min(size.y() / 2.) // Shrink to half of the editor height
-                            .max(4. * line_height), // Apply minimum height of 4 lines
-                    ),
-                },
-                cx,
-            );
+        if let Some((_, hover_popovers)) = hover.as_mut() {
+            for hover_popover in hover_popovers.iter_mut() {
+                hover_popover.layout(
+                    SizeConstraint {
+                        min: Vector2F::zero(),
+                        max: vec2f(
+                            (120. * em_width) // Default size
+                                .min(size.x() / 2.) // Shrink to half of the editor width
+                                .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
+                            (16. * line_height) // Default size
+                                .min(size.y() / 2.) // Shrink to half of the editor height
+                                .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
+                        ),
+                    },
+                    cx,
+                );
+            }
         }
 
         (
@@ -1391,7 +1435,7 @@ impl Element for EditorElement {
                 selections,
                 context_menu,
                 code_actions_indicator,
-                hover,
+                hover_popovers: hover,
             },
         )
     }
@@ -1416,7 +1460,7 @@ impl Element for EditorElement {
             gutter_bounds,
             text_bounds,
             context_menu_bounds: None,
-            hover_bounds: None,
+            hover_popover_bounds: Default::default(),
         };
 
         self.paint_background(gutter_bounds, text_bounds, layout, cx);
@@ -1457,9 +1501,11 @@ impl Element for EditorElement {
             }
         }
 
-        if let Some((_, hover)) = &mut layout.hover {
-            if hover.dispatch_event(event, cx) {
-                return true;
+        if let Some((_, popover_elements)) = &mut layout.hover_popovers {
+            for popover_element in popover_elements.iter_mut() {
+                if popover_element.dispatch_event(event, cx) {
+                    return true;
+                }
             }
         }
 
@@ -1590,7 +1636,7 @@ pub struct LayoutState {
     selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
     context_menu: Option<(DisplayPoint, ElementBox)>,
     code_actions_indicator: Option<(u32, ElementBox)>,
-    hover: Option<(DisplayPoint, ElementBox)>,
+    hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
 }
 
 struct BlockLayout {
@@ -1635,7 +1681,7 @@ pub struct PaintState {
     gutter_bounds: RectF,
     text_bounds: RectF,
     context_menu_bounds: Option<RectF>,
-    hover_bounds: Option<RectF>,
+    hover_popover_bounds: Vec<RectF>,
 }
 
 impl PaintState {

crates/editor/src/hover_popover.rs 🔗

@@ -3,9 +3,10 @@ use gpui::{
     elements::{Flex, MouseEventHandler, Padding, Text},
     impl_internal_actions,
     platform::CursorStyle,
-    Axis, Element, ElementBox, ModelHandle, MutableAppContext, RenderContext, Task, ViewContext,
+    Axis, Element, ElementBox, ModelHandle, MouseButton, MutableAppContext, RenderContext, Task,
+    ViewContext,
 };
-use language::Bias;
+use language::{Bias, DiagnosticEntry, DiagnosticSeverity};
 use project::{HoverBlock, Project};
 use settings::Settings;
 use std::{ops::Range, time::Duration};
@@ -13,7 +14,7 @@ use util::TryFutureExt;
 
 use crate::{
     display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
-    EditorStyle,
+    EditorStyle, GoToDiagnostic, RangeToAnchorExt,
 };
 
 pub const HOVER_DELAY_MILLIS: u64 = 350;
@@ -54,17 +55,11 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
 /// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 /// selections changed.
 pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
-    let mut did_hide = false;
+    let did_hide = editor.hover_state.info_popover.take().is_some()
+        | editor.hover_state.diagnostic_popover.take().is_some();
 
-    // only notify the context once
-    if editor.hover_state.popover.is_some() {
-        editor.hover_state.popover = None;
-        did_hide = true;
-        cx.notify();
-    }
-    editor.hover_state.task = None;
+    editor.hover_state.info_task = None;
     editor.hover_state.triggered_from = None;
-    editor.hover_state.symbol_range = None;
 
     editor.clear_background_highlights::<HoverState>(cx);
 
@@ -114,8 +109,8 @@ fn show_hover(
     };
 
     if !ignore_timeout {
-        if let Some(range) = &editor.hover_state.symbol_range {
-            if range
+        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
+            if symbol_range
                 .to_offset(&snapshot.buffer_snapshot)
                 .contains(&multibuffer_offset)
             {
@@ -167,6 +162,43 @@ fn show_hover(
                 })
             });
 
+            if let Some(delay) = delay {
+                delay.await;
+            }
+
+            // If there's a diagnostic, assign it on the hover state and notify
+            let local_diagnostic = snapshot
+                .buffer_snapshot
+                .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
+                // Find the entry with the most specific range
+                .min_by_key(|entry| entry.range.end - entry.range.start)
+                .map(|entry| DiagnosticEntry {
+                    diagnostic: entry.diagnostic,
+                    range: entry.range.to_anchors(&snapshot.buffer_snapshot),
+                });
+
+            // Pull the primary diagnostic out so we can jump to it if the popover is clicked
+            let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
+                snapshot
+                    .buffer_snapshot
+                    .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
+                    .find(|diagnostic| diagnostic.diagnostic.is_primary)
+                    .map(|entry| DiagnosticEntry {
+                        diagnostic: entry.diagnostic,
+                        range: entry.range.to_anchors(&snapshot.buffer_snapshot),
+                    })
+            });
+
+            if let Some(this) = this.upgrade(&cx) {
+                this.update(&mut cx, |this, _| {
+                    this.hover_state.diagnostic_popover =
+                        local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
+                            local_diagnostic,
+                            primary_diagnostic,
+                        });
+                });
+            }
+
             // Construct new hover popover from hover request
             let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
                 if hover_result.contents.is_empty() {
@@ -188,45 +220,28 @@ fn show_hover(
                     anchor.clone()..anchor.clone()
                 };
 
-                if let Some(this) = this.upgrade(&cx) {
-                    this.update(&mut cx, |this, _| {
-                        this.hover_state.symbol_range = Some(range.clone());
-                    });
-                }
-
-                Some(HoverPopover {
+                Some(InfoPopover {
                     project: project.clone(),
-                    anchor: range.start.clone(),
+                    symbol_range: range.clone(),
                     contents: hover_result.contents,
                 })
             });
 
-            if let Some(delay) = delay {
-                delay.await;
-            }
-
             if let Some(this) = this.upgrade(&cx) {
                 this.update(&mut cx, |this, cx| {
-                    if hover_popover.is_some() {
+                    if let Some(hover_popover) = hover_popover.as_ref() {
                         // Highlight the selected symbol using a background highlight
-                        if let Some(range) = this.hover_state.symbol_range.clone() {
-                            this.highlight_background::<HoverState>(
-                                vec![range],
-                                |theme| theme.editor.hover_popover.highlight,
-                                cx,
-                            );
-                        }
-                        this.hover_state.popover = hover_popover;
-                        cx.notify();
+                        this.highlight_background::<HoverState>(
+                            vec![hover_popover.symbol_range.clone()],
+                            |theme| theme.editor.hover_popover.highlight,
+                            cx,
+                        );
                     } else {
-                        if this.hover_state.visible() {
-                            // Popover was visible, but now is hidden. Dismiss it
-                            hide_hover(this, cx);
-                        } else {
-                            // Clear selected symbol range for future requests
-                            this.hover_state.symbol_range = None;
-                        }
+                        this.clear_background_highlights::<HoverState>(cx);
                     }
+
+                    this.hover_state.info_popover = hover_popover;
+                    cx.notify();
                 });
             }
             Ok::<_, anyhow::Error>(())
@@ -234,38 +249,70 @@ fn show_hover(
         .log_err()
     });
 
-    editor.hover_state.task = Some(task);
+    editor.hover_state.info_task = Some(task);
 }
 
 #[derive(Default)]
 pub struct HoverState {
-    pub popover: Option<HoverPopover>,
+    pub info_popover: Option<InfoPopover>,
+    pub diagnostic_popover: Option<DiagnosticPopover>,
     pub triggered_from: Option<Anchor>,
-    pub symbol_range: Option<Range<Anchor>>,
-    pub task: Option<Task<Option<()>>>,
+    pub info_task: Option<Task<Option<()>>>,
 }
 
 impl HoverState {
     pub fn visible(&self) -> bool {
-        self.popover.is_some()
+        self.info_popover.is_some() || self.diagnostic_popover.is_some()
+    }
+
+    pub fn render(
+        &self,
+        snapshot: &EditorSnapshot,
+        style: &EditorStyle,
+        visible_rows: Range<u32>,
+        cx: &mut RenderContext<Editor>,
+    ) -> Option<(DisplayPoint, Vec<ElementBox>)> {
+        // If there is a diagnostic, position the popovers based on that.
+        // Otherwise use the start of the hover range
+        let anchor = self
+            .diagnostic_popover
+            .as_ref()
+            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
+            .or_else(|| {
+                self.info_popover
+                    .as_ref()
+                    .map(|info_popover| &info_popover.symbol_range.start)
+            })?;
+        let point = anchor.to_display_point(&snapshot.display_snapshot);
+
+        // Don't render if the relevant point isn't on screen
+        if !self.visible() || !visible_rows.contains(&point.row()) {
+            return None;
+        }
+
+        let mut elements = Vec::new();
+
+        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
+            elements.push(diagnostic_popover.render(style, cx));
+        }
+        if let Some(info_popover) = self.info_popover.as_ref() {
+            elements.push(info_popover.render(style, cx));
+        }
+
+        Some((point, elements))
     }
 }
 
 #[derive(Debug, Clone)]
-pub struct HoverPopover {
+pub struct InfoPopover {
     pub project: ModelHandle<Project>,
-    pub anchor: Anchor,
+    pub symbol_range: Range<Anchor>,
     pub contents: Vec<HoverBlock>,
 }
 
-impl HoverPopover {
-    pub fn render(
-        &self,
-        snapshot: &EditorSnapshot,
-        style: EditorStyle,
-        cx: &mut RenderContext<Editor>,
-    ) -> (DisplayPoint, ElementBox) {
-        let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
+impl InfoPopover {
+    pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
+        MouseEventHandler::new::<InfoPopover, _, _>(0, cx, |_, cx| {
             let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
             flex.extend(self.contents.iter().map(|content| {
                 let project = self.project.read(cx);
@@ -309,10 +356,61 @@ impl HoverPopover {
             top: 5.,
             ..Default::default()
         })
-        .boxed();
+        .boxed()
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct DiagnosticPopover {
+    local_diagnostic: DiagnosticEntry<Anchor>,
+    primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
+}
+
+impl DiagnosticPopover {
+    pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
+        enum PrimaryDiagnostic {}
+
+        let mut text_style = style.hover_popover.prose.clone();
+        text_style.font_size = style.text.font_size;
+
+        let container_style = match self.local_diagnostic.diagnostic.severity {
+            DiagnosticSeverity::HINT => style.hover_popover.info_container,
+            DiagnosticSeverity::INFORMATION => style.hover_popover.info_container,
+            DiagnosticSeverity::WARNING => style.hover_popover.warning_container,
+            DiagnosticSeverity::ERROR => style.hover_popover.error_container,
+            _ => style.hover_popover.container,
+        };
 
-        let display_point = self.anchor.to_display_point(&snapshot.display_snapshot);
-        (display_point, element)
+        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+
+        MouseEventHandler::new::<DiagnosticPopover, _, _>(0, cx, |_, _| {
+            Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style)
+                .with_soft_wrap(true)
+                .contained()
+                .with_style(container_style)
+                .boxed()
+        })
+        .on_click(MouseButton::Left, |_, cx| {
+            cx.dispatch_action(GoToDiagnostic)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<PrimaryDiagnostic, _>(
+            0,
+            "Go To Diagnostic".to_string(),
+            Some(Box::new(crate::GoToDiagnostic)),
+            tooltip_style,
+            cx,
+        )
+        .boxed()
+    }
+
+    pub fn activation_info(&self) -> (usize, Anchor) {
+        let entry = self
+            .primary_diagnostic
+            .as_ref()
+            .unwrap_or(&self.local_diagnostic);
+
+        (entry.diagnostic.group_id, entry.range.start.clone())
     }
 }
 
@@ -321,6 +419,7 @@ mod tests {
     use futures::StreamExt;
     use indoc::indoc;
 
+    use language::{Diagnostic, DiagnosticSet};
     use project::HoverBlock;
 
     use crate::test::EditorLspTestContext;
@@ -328,7 +427,7 @@ mod tests {
     use super::*;
 
     #[gpui::test]
-    async fn test_hover_popover(cx: &mut gpui::TestAppContext) {
+    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
         let mut cx = EditorLspTestContext::new_rust(
             lsp::ServerCapabilities {
                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -362,19 +461,18 @@ mod tests {
             fn test()
                 [println!]();"});
         let mut requests =
-            cx.lsp
-                .handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
-                    Ok(Some(lsp::Hover {
-                        contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-                            kind: lsp::MarkupKind::Markdown,
-                            value: indoc! {"
+            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+                Ok(Some(lsp::Hover {
+                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                        kind: lsp::MarkupKind::Markdown,
+                        value: indoc! {"
                                 # Some basic docs
                                 Some test documentation"}
-                            .to_string(),
-                        }),
-                        range: Some(symbol_range),
-                    }))
-                });
+                        .to_string(),
+                    }),
+                    range: Some(symbol_range),
+                }))
+            });
         cx.foreground()
             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
         requests.next().await;
@@ -382,7 +480,7 @@ mod tests {
         cx.editor(|editor, _| {
             assert!(editor.hover_state.visible());
             assert_eq!(
-                editor.hover_state.popover.clone().unwrap().contents,
+                editor.hover_state.info_popover.clone().unwrap().contents,
                 vec![
                     HoverBlock {
                         text: "Some basic docs".to_string(),
@@ -400,6 +498,9 @@ mod tests {
         let hover_point = cx.display_point(indoc! {"
             fn te|st()
                 println!();"});
+        let mut request = cx
+            .lsp
+            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
         cx.update_editor(|editor, cx| {
             hover_at(
                 editor,
@@ -409,15 +510,24 @@ mod tests {
                 cx,
             )
         });
-        let mut request = cx
-            .lsp
-            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
         cx.foreground()
             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
         request.next().await;
         cx.editor(|editor, _| {
             assert!(!editor.hover_state.visible());
         });
+    }
+
+    #[gpui::test]
+    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
 
         // Hover with keyboard has no delay
         cx.set_state(indoc! {"
@@ -427,26 +537,25 @@ mod tests {
         let symbol_range = cx.lsp_range(indoc! {"
             [fn] test()
                 println!();"});
-        cx.lsp
-            .handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
-                Ok(Some(lsp::Hover {
-                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-                        kind: lsp::MarkupKind::Markdown,
-                        value: indoc! {"
-                            # Some other basic docs
-                            Some other test documentation"}
-                        .to_string(),
-                    }),
-                    range: Some(symbol_range),
-                }))
-            })
-            .next()
-            .await;
-        cx.foreground().run_until_parked();
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                    kind: lsp::MarkupKind::Markdown,
+                    value: indoc! {"
+                        # Some other basic docs
+                        Some other test documentation"}
+                    .to_string(),
+                }),
+                range: Some(symbol_range),
+            }))
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
         cx.editor(|editor, _| {
-            assert!(editor.hover_state.visible());
             assert_eq!(
-                editor.hover_state.popover.clone().unwrap().contents,
+                editor.hover_state.info_popover.clone().unwrap().contents,
                 vec![
                     HoverBlock {
                         text: "Some other basic docs".to_string(),
@@ -460,4 +569,73 @@ mod tests {
             )
         });
     }
+
+    #[gpui::test]
+    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
+        // info popover once request completes
+        cx.set_state(indoc! {"
+            fn te|st()
+                println!();"});
+
+        // Send diagnostic to client
+        let range = cx.text_anchor_range(indoc! {"
+            fn [test]()
+                println!();"});
+        cx.update_buffer(|buffer, cx| {
+            let snapshot = buffer.text_snapshot();
+            let set = DiagnosticSet::from_sorted_entries(
+                vec![DiagnosticEntry {
+                    range,
+                    diagnostic: Diagnostic {
+                        message: "A test diagnostic message.".to_string(),
+                        ..Default::default()
+                    },
+                }],
+                &snapshot,
+            );
+            buffer.update_diagnostics(set, cx);
+        });
+
+        // Hover pops diagnostic immediately
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        cx.foreground().run_until_parked();
+
+        cx.editor(|Editor { hover_state, .. }, _| {
+            assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
+        });
+
+        // Info Popover shows after request responded to
+        let range = cx.lsp_range(indoc! {"
+            fn [test]()
+                println!();"});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                    kind: lsp::MarkupKind::Markdown,
+                    value: indoc! {"
+                    # Some other basic docs
+                    Some other test documentation"}
+                    .to_string(),
+                }),
+                range: Some(range),
+            }))
+        });
+        cx.foreground()
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+
+        cx.foreground().run_until_parked();
+        cx.editor(|Editor { hover_state, .. }, _| {
+            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
+        });
+    }
 }

crates/editor/src/test.rs 🔗

@@ -9,9 +9,13 @@ use futures::{Future, StreamExt};
 use indoc::indoc;
 
 use collections::BTreeMap;
-use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
-use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
-use lsp::request;
+use gpui::{
+    json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
+};
+use language::{
+    point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig, Selection,
+};
+use lsp::{notification, request};
 use project::Project;
 use settings::Settings;
 use util::{
@@ -119,7 +123,7 @@ impl<'a> EditorTestContext<'a> {
         self.editor.condition(self.cx, predicate)
     }
 
-    pub fn editor<F, T>(&mut self, read: F) -> T
+    pub fn editor<F, T>(&self, read: F) -> T
     where
         F: FnOnce(&Editor, &AppContext) -> T,
     {
@@ -133,9 +137,31 @@ impl<'a> EditorTestContext<'a> {
         self.editor.update(self.cx, update)
     }
 
-    pub fn buffer_text(&mut self) -> String {
-        self.editor.read_with(self.cx, |editor, cx| {
-            editor.buffer.read(cx).snapshot(cx).text()
+    pub fn multibuffer<F, T>(&self, read: F) -> T
+    where
+        F: FnOnce(&MultiBuffer, &AppContext) -> T,
+    {
+        self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
+    }
+
+    pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
+    {
+        self.update_editor(|editor, cx| editor.buffer().update(cx, update))
+    }
+
+    pub fn buffer_text(&self) -> String {
+        self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
+    }
+
+    pub fn buffer<F, T>(&self, read: F) -> T
+    where
+        F: FnOnce(&Buffer, &AppContext) -> T,
+    {
+        self.multibuffer(|multibuffer, cx| {
+            let buffer = multibuffer.as_singleton().unwrap().read(cx);
+            read(buffer, cx)
         })
     }
 
@@ -145,6 +171,20 @@ impl<'a> EditorTestContext<'a> {
         });
     }
 
+    pub fn update_buffer<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
+    {
+        self.update_multibuffer(|multibuffer, cx| {
+            let buffer = multibuffer.as_singleton().unwrap();
+            buffer.update(cx, update)
+        })
+    }
+
+    pub fn buffer_snapshot(&self) -> BufferSnapshot {
+        self.buffer(|buffer, _| buffer.snapshot())
+    }
+
     pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
         let keystroke = Keystroke::parse(keystroke_text).unwrap();
         self.cx.dispatch_keystroke(self.window_id, keystroke, false);
@@ -164,6 +204,18 @@ impl<'a> EditorTestContext<'a> {
         locations[0].to_display_point(&snapshot.display_snapshot)
     }
 
+    // Returns anchors for the current buffer using `[`..`]`
+    pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
+        let range_marker: TextRangeMarker = ('[', ']').into();
+        let (unmarked_text, mut ranges) =
+            marked_text_ranges_by(&marked_text, vec![range_marker.clone()]);
+        assert_eq!(self.buffer_text(), unmarked_text);
+        let offset_range = ranges.remove(&range_marker).unwrap()[0].clone();
+        let snapshot = self.buffer_snapshot();
+
+        snapshot.anchor_before(offset_range.start)..snapshot.anchor_after(offset_range.end)
+    }
+
     // Sets the editor state via a marked string.
     // `|` characters represent empty selections
     // `[` to `}` represents a non empty selection with the head at `}`
@@ -433,7 +485,7 @@ pub struct EditorLspTestContext<'a> {
     pub cx: EditorTestContext<'a>,
     pub lsp: lsp::FakeLanguageServer,
     pub workspace: ViewHandle<Workspace>,
-    pub editor_lsp_url: lsp::Url,
+    pub buffer_lsp_url: lsp::Url,
 }
 
 impl<'a> EditorLspTestContext<'a> {
@@ -507,7 +559,7 @@ impl<'a> EditorLspTestContext<'a> {
             },
             lsp,
             workspace,
-            editor_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
+            buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
         }
     }
 
@@ -530,7 +582,7 @@ impl<'a> EditorLspTestContext<'a> {
     // Constructs lsp range using a marked string with '[', ']' range delimiters
     pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
         let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
-        assert_eq!(unmarked, self.cx.buffer_text());
+        assert_eq!(unmarked, self.buffer_text());
         let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
         self.to_lsp_range(offset_range)
     }
@@ -594,12 +646,16 @@ impl<'a> EditorLspTestContext<'a> {
         F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
         Fut: 'static + Send + Future<Output = Result<T::Result>>,
     {
-        let url = self.editor_lsp_url.clone();
+        let url = self.buffer_lsp_url.clone();
         self.lsp.handle_request::<T, _, _>(move |params, cx| {
             let url = url.clone();
             handler(url, params, cx)
         })
     }
+
+    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
+        self.lsp.notify::<T>(params);
+    }
 }
 
 impl<'a> Deref for EditorLspTestContext<'a> {

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::<FileFinder>()
-                .unwrap()
-        });
+        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
         finder
             .update(cx, |finder, cx| {
                 finder.update_matches("bna".to_string(), cx)

crates/language/src/buffer.rs 🔗

@@ -2466,11 +2466,11 @@ impl operation_queue::Operation for Operation {
 impl Default for Diagnostic {
     fn default() -> Self {
         Self {
-            code: Default::default(),
+            code: None,
             severity: DiagnosticSeverity::ERROR,
             message: Default::default(),
-            group_id: Default::default(),
-            is_primary: Default::default(),
+            group_id: 0,
+            is_primary: false,
             is_valid: true,
             is_disk_based: false,
             is_unnecessary: false,

crates/settings/src/settings.rs 🔗

@@ -81,7 +81,7 @@ pub struct TerminalSettings {
     pub working_directory: Option<WorkingDirectory>,
     pub font_size: Option<f32>,
     pub font_family: Option<String>,
-    pub env: Option<Vec<(String, String)>>,
+    pub env: Option<HashMap<String, String>>,
 }
 
 #[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]

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"] }

crates/terminal/src/connected_el.rs 🔗

@@ -0,0 +1,853 @@
+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<LayoutCell>,
+    rects: Vec<LayoutRect>,
+    highlights: Vec<RelativeHighlightedRange>,
+    cursor: Option<Cursor>,
+    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<WindowSize> 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<i32, i32>,
+    text: Line,
+}
+
+impl LayoutCell {
+    fn new(point: Point<i32, i32>, 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<i32, i32>,
+    num_of_cells: usize,
+    color: Color,
+}
+
+impl LayoutRect {
+    fn new(point: Point<i32, i32>, 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<i32, i32>, 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<usize>,
+}
+
+impl RelativeHighlightedRange {
+    fn new(line_index: usize, range: Range<usize>) -> 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<Terminal>,
+    view: WeakViewHandle<ConnectedView>,
+    modal: bool,
+}
+
+impl TerminalEl {
+    pub fn new(
+        view: WeakViewHandle<ConnectedView>,
+        terminal: WeakModelHandle<Terminal>,
+        modal: bool,
+    ) -> TerminalEl {
+        TerminalEl {
+            view,
+            terminal,
+            modal,
+        }
+    }
+
+    fn layout_grid(
+        grid: GridIterator<Cell>,
+        text_style: &TextStyle,
+        terminal_theme: &TerminalStyle,
+        text_layout_cache: &TextLayoutCache,
+        modal: bool,
+        selection_range: Option<SelectionRange>,
+    ) -> (
+        Vec<LayoutCell>,
+        Vec<LayoutRect>,
+        Vec<RelativeHighlightedRange>,
+    ) {
+        let mut cells = vec![];
+        let mut rects = vec![];
+        let mut highlight_ranges = vec![];
+
+        let mut cur_rect: Option<LayoutRect> = 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::<Settings>();
+        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::<Vec<HighlightedRangeLine>>();
+
+                    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",
+        })
+    }
+
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        bounds: RectF,
+        _: RectF,
+        layout: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &gpui::MeasurementContext,
+    ) -> Option<RectF> {
+        // Use the same origin that's passed to `Cursor::paint` in the paint
+        // method bove.
+        let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
+
+        // TODO - Why is it necessary to move downward one line to get correct
+        // positioning? I would think that we'd want the same rect that is
+        // painted for the cursor.
+        origin += vec2f(0., layout.size.line_height);
+
+        Some(layout.cursor.as_ref()?.bounding_rect(origin))
+    }
+}
+
+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),
+            )
+        );
+    }
+}

crates/terminal/src/connected_view.rs 🔗

@@ -0,0 +1,176 @@
+use gpui::{
+    actions, keymap::Keystroke, AppContext, 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<Terminal>,
+    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<Terminal>,
+        modal: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> 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<Terminal> {
+        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<ConnectedView>) {
+        self.has_bell = false;
+        cx.emit(Event::TitleChanged);
+    }
+
+    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
+        self.terminal.read(cx).clear();
+    }
+
+    ///Attempt to paste the clipboard into the terminal
+    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
+        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<Self>) {
+        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>) {
+        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>) {
+        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>) {
+        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>) {
+        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>) {
+        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>) {
+        self.has_new_content = false;
+    }
+
+    fn selected_text_range(&self, _: &AppContext) -> Option<std::ops::Range<usize>> {
+        Some(0..0)
+    }
+
+    fn replace_text_in_range(
+        &mut self,
+        _: Option<std::ops::Range<usize>>,
+        text: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.terminal
+            .update(cx, |terminal, _| terminal.write_to_pty(text.into()));
+    }
+}

crates/terminal/src/connection.rs 🔗

@@ -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<FairMutex<Term<ZedListener>>>,
-    pub title: String,
-    pub associated_directory: Option<PathBuf>,
-}
-
-impl TerminalConnection {
-    pub fn new(
-        working_directory: Option<PathBuf>,
-        shell: Option<Shell>,
-        env_vars: Option<Vec<(String, String)>>,
-        initial_size: SizeInfo,
-        cx: &mut ModelContext<Self>,
-    ) -> 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<String, String> = 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<Self>,
-    ) {
-        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::<Settings>().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<u8>) {
-        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;
-}

crates/terminal/src/color_translation.rs → 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);
         }
     }

crates/terminal/src/connection/keymappings.rs → 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,
@@ -313,6 +304,20 @@ mod test {
         assert_eq!(to_esc_str(&pagedown, &any), Some("\x1b[6~".to_string()));
     }
 
+    #[test]
+    fn test_multi_char_fallthrough() {
+        let ks = Keystroke {
+            ctrl: false,
+            alt: false,
+            shift: false,
+            cmd: false,
+
+            key: "🖖🏻".to_string(), //2 char string
+        };
+
+        assert_eq!(to_esc_str(&ks, &TermMode::NONE), Some("🖖🏻".to_string()));
+    }
+
     #[test]
     fn test_application_mode() {
         let app_cursor = TermMode::APP_CURSOR;

crates/terminal/src/modal.rs 🔗

@@ -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<TerminalConnection>);
-
-pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
-    // Pull the terminal connection out of the global if it has been stored
-    let possible_connection =
-        cx.update_default_global::<Option<StoredConnection>, _, _>(|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::<Option<StoredConnection>>(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::<Option<StoredConnection>>(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<TerminalConnection>,
-    event: &Event,
-    cx: &mut ViewContext<Workspace>,
-) {
-    // Dismiss the modal if the terminal quit
-    if let Event::CloseTerminal = event {
-        cx.set_global::<Option<StoredConnection>>(None);
-        if workspace
-            .modal()
-            .cloned()
-            .and_then(|modal| modal.downcast::<Terminal>())
-            .is_some()
-        {
-            workspace.dismiss_modal(cx)
-        }
-    }
-}

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<Terminal>);
+
+pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
+    // Pull the terminal connection out of the global if it has been stored
+    let possible_terminal =
+        cx.update_default_global::<Option<StoredTerminal>, _, _>(|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::<Option<StoredTerminal>>(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::<Option<StoredTerminal>>(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::<Option<StoredTerminal>>(Some(StoredTerminal(
+                    terminal_handle.clone(),
+                )));
+            }
+        }
+    }
+}
+
+pub fn on_event(
+    workspace: &mut Workspace,
+    _: ModelHandle<Terminal>,
+    event: &Event,
+    cx: &mut ViewContext<Workspace>,
+) {
+    // Dismiss the modal if the terminal quit
+    if let Event::CloseTerminal = event {
+        cx.set_global::<Option<StoredTerminal>>(None);
+        if workspace.modal::<TerminalView>().is_some() {
+            workspace.dismiss_modal(cx)
+        }
+    }
+}

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<AlacTermEvent>);
+
+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<PathBuf>,
+    pub shell: Option<Shell>,
+    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!("<non-utf8 path> {}", 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!("<none specified, using home directory> {}", dir),
+                    None => "<none specified, could not find home directory>".to_string(),
+                }
+            })
+    }
+
+    pub fn shell_to_string(&self) -> Option<String> {
+        self.shell.as_ref().map(|shell| match shell {
+            Shell::System => "<system shell>".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!("<system defined shell> {}", pw.shell),
+                        None => "<could not access the password file>".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!("<none specified, using system defined shell> {}", pw.shell)
+                    }
+                    None => "<none specified, could not access the password file> {}".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<AlacTermEvent>,
+}
+
+impl TerminalBuilder {
+    pub fn new(
+        working_directory: Option<PathBuf>,
+        shell: Option<Shell>,
+        env: Option<HashMap<String, String>>,
+        initial_size: TermDimensions,
+    ) -> Result<TerminalBuilder> {
+        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>) -> 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<FairMutex<Term<ZedListener>>>,
+    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<Terminal>,
+    ) {
+        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::<Settings>().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<u8>) {
+        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<String> {
+        let term = self.term.lock();
+        term.selection_to_string()
+    }
+
+    ///Takes the selection out of the terminal
+    pub fn take_selection(&self) -> Option<Selection> {
+        self.term.lock().selection.take()
+    }
+    ///Sets the selection object on the terminal
+    pub fn set_selection(&self, sel: Option<Selection>) {
+        self.term.lock().selection = sel;
+    }
+
+    pub fn render_lock<F, T>(&self, new_size: Option<TermDimensions>, 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<Passwd<'_>> {
+        // Create zeroed passwd struct.
+        let mut entry: MaybeUninit<libc::passwd> = 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()))
+    }
+}

crates/terminal/src/terminal.rs 🔗

@@ -1,257 +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 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 terminal_element::TerminalEl;
 use workspace::{Item, Workspace};
 
+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::clear);
+
+    connected_view::init(cx);
 }
 
-///A translation struct for Alacritty to communicate with us from their event loop
-#[derive(Clone)]
-pub struct ZedListener(UnboundedSender<AlacTermEvent>);
+//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
 
-impl EventListener for ZedListener {
-    fn send_event(&self, event: AlacTermEvent) {
-        self.0.unbounded_send(event).ok();
+enum TerminalContent {
+    Connected(ViewHandle<ConnectedView>),
+    Error(ViewHandle<ErrorView>),
+}
+
+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<TerminalConnection>,
-    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<PathBuf>,
 }
 
-impl Entity for Terminal {
+pub struct ErrorView {
+    error: TerminalError,
+}
+
+impl Entity for TerminalView {
+    type Event = Event;
+}
+
+impl Entity for ConnectedView {
     type Event = Event;
 }
 
-impl Terminal {
+impl Entity for ErrorView {
+    type Event = Event;
+}
+
+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<Workspace>) {
+        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<PathBuf>, modal: bool, cx: &mut ViewContext<Self>) -> 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::<Settings>();
-            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::<Settings>();
+        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::<TerminalError>().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<TerminalConnection>,
+    fn from_terminal(
+        terminal: ModelHandle<Terminal>,
         modal: bool,
         cx: &mut ViewContext<Self>,
-    ) -> 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, text: &str, cx: &mut ViewContext<Self>) {
-        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.to_string());
-        });
-
-        if self.has_bell {
-            self.has_bell = false;
-            cx.emit(Event::TitleChanged);
-        }
-    }
-
-    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
-        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<Workspace>) {
-        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<Self>) {
-        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<Self>) {
-        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>) {
-        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>) {
-        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>) {
-        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>) {
-        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>) {
-        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::<Settings>();
             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<Self>) {
         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 {
@@ -261,67 +172,83 @@ impl View for Terminal {
         }
         context
     }
+}
 
-    fn selected_text_range(&self, _: &AppContext) -> Option<std::ops::Range<usize>> {
-        Some(0..0)
+impl View for ErrorView {
+    fn ui_name() -> &'static str {
+        "Terminal Error"
     }
 
-    fn replace_text_in_range(
-        &mut self,
-        _: Option<std::ops::Range<usize>>,
-        text: &str,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.input(text, cx);
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+        let settings = cx.global::<Settings>();
+        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 Terminal {
+impl Item for TerminalView {
     fn tab_content(
         &self,
         _detail: Option<usize>,
         tab_theme: &theme::Tab,
         cx: &gpui::AppContext,
     ) -> ElementBox {
-        let settings = cx.global::<Settings>();
-        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<Self>) -> Option<Self> {
         //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,
         ))
@@ -370,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 {
@@ -388,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<PathBuf> {
+fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
     let wd_setting = cx
         .global::<Settings>()
         .terminal_overrides
@@ -399,10 +338,12 @@ fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBu
         WorkingDirectory::CurrentProjectDirectory => 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())
 }
@@ -447,7 +388,6 @@ mod tests {
     use gpui::TestAppContext;
 
     use std::path::Path;
-    use workspace::AppState;
 
     mod terminal_test_context;
 
@@ -455,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;
@@ -467,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();
 
@@ -491,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();
 
@@ -531,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();
 
@@ -569,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();
 
@@ -632,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();
 

crates/terminal/src/terminal_element.rs 🔗

@@ -1,828 +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<TerminalConnection>,
-    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<LayoutCell>,
-    highlighted_range: Option<Range<usize>>,
-}
-
-///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<i32, i32>,
-    text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN!
-    background_color: Color,
-}
-
-impl LayoutCell {
-    fn new(point: Point<i32, i32>, 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<LayoutLine>,
-    line_height: LineHeight,
-    em_width: CellWidth,
-    cursor: Option<Cursor>,
-    background_color: Color,
-    cur_size: SizeInfo,
-    terminal: Arc<FairMutex<Term<ZedListener>>>,
-    selection_color: Color,
-}
-
-impl TerminalEl {
-    pub fn new(
-        view_id: usize,
-        connection: WeakModelHandle<TerminalConnection>,
-        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::<Settings>());
-        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::<Settings>()).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::<Vec<HighlightedRangeLine>>();
-
-                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 rect_for_text_range(
-        &self,
-        _: Range<usize>,
-        bounds: RectF,
-        _: RectF,
-        layout: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &gpui::MeasurementContext,
-    ) -> Option<RectF> {
-        // Use the same origin that's passed to `Cursor::paint` in the paint
-        // method bove.
-        let mut origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
-
-        // TODO - Why is it necessary to move downward one line to get correct
-        // positioning? I would think that we'd want the same rect that is
-        // painted for the cursor.
-        origin += vec2f(0., layout.line_height.0);
-
-        Some(layout.cursor.as_ref()?.bounding_rect(origin))
-    }
-
-    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<Cell>,
-    text_style: &TextStyle,
-    terminal_theme: &TerminalStyle,
-    text_layout_cache: &TextLayoutCache,
-    modal: bool,
-    selection_range: Option<SelectionRange>,
-) -> Vec<LayoutLine> {
-    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::<Vec<LayoutCell>>();
-
-            LayoutLine {
-                cells,
-                highlighted_range,
-            }
-        })
-        .collect::<Vec<LayoutLine>>()
-}
-
-// 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<FairMutex<Term<ZedListener>>>,
-    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),
-            )
-        );
-    }
-}

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<TerminalConnection>,
+    pub connection: Option<ModelHandle<Terminal>>,
 }
 
 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<Project>, ViewHandle<Workspace>) {
+        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<Project>,
+        path: impl AsRef<Path>,
+    ) -> (ModelHandle<Worktree>, 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<Project>,
+        path: impl AsRef<Path>,
+    ) -> (ModelHandle<Worktree>, Entry) {
+        self.create_wt(project, false, path).await
+    }
+
+    async fn create_wt(
+        &mut self,
+        project: ModelHandle<Project>,
+        is_dir: bool,
+        path: impl AsRef<Path>,
+    ) -> (ModelHandle<Worktree>, 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<Worktree>,
+        entry: Entry,
+        project: ModelHandle<Project>,
+    ) {
+        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::<String>())
-            .collect::<Vec<String>>()
-            .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::<String>())
+                .collect::<Vec<String>>()
+                .join("\n")
+        })
     }
 }
 

crates/text/src/selection.rs 🔗

@@ -54,6 +54,13 @@ impl<T: Clone> Selection<T> {
             goal: self.goal,
         }
     }
+
+    pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) {
+        self.start = point.clone();
+        self.end = point;
+        self.goal = new_goal;
+        self.reversed = false;
+    }
 }
 
 impl<T: Copy + Ord> Selection<T> {
@@ -78,13 +85,6 @@ impl<T: Copy + Ord> Selection<T> {
         self.goal = new_goal;
     }
 
-    pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) {
-        self.start = point;
-        self.end = point;
-        self.goal = new_goal;
-        self.reversed = false;
-    }
-
     pub fn range(&self) -> Range<T> {
         self.start..self.end
     }

crates/theme/src/theme.rs 🔗

@@ -630,6 +630,9 @@ impl<'de> Deserialize<'de> for SyntaxTheme {
 #[derive(Clone, Deserialize, Default)]
 pub struct HoverPopover {
     pub container: ContainerStyle,
+    pub info_container: ContainerStyle,
+    pub warning_container: ContainerStyle,
+    pub error_container: ContainerStyle,
     pub block_style: ContainerStyle,
     pub prose: TextStyle,
     pub highlight: Color,

crates/workspace/src/workspace.rs 🔗

@@ -1224,8 +1224,10 @@ impl Workspace {
         }
     }
 
-    pub fn modal(&self) -> Option<&AnyViewHandle> {
-        self.modal.as_ref()
+    pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
+        self.modal
+            .as_ref()
+            .and_then(|modal| modal.clone().downcast::<V>())
     }
 
     pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {

crates/zed/src/menus.rs 🔗

@@ -285,7 +285,7 @@ pub fn menus() -> Vec<Menu<'static>> {
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Next Problem",
-                    action: Box::new(editor::GoToNextDiagnostic),
+                    action: Box::new(editor::GoToDiagnostic),
                 },
                 MenuItem::Action {
                     name: "Previous Problem",

styles/src/styleTree/hoverPopover.ts 🔗

@@ -2,22 +2,48 @@ import Theme from "../themes/common/theme";
 import { backgroundColor, border, popoverShadow, text } from "./components";
 
 export default function HoverPopover(theme: Theme) {
+  let baseContainer = {
+    background: backgroundColor(theme, "on500"),
+    cornerRadius: 8,
+    padding: {
+      left: 8,
+      right: 8,
+      top: 4,
+      bottom: 4
+    },
+    shadow: popoverShadow(theme),
+    border: border(theme, "secondary"),
+    margin: {
+      left: -8,
+    },
+  };
+
   return {
-    container: {
-      background: backgroundColor(theme, "on500"),
-      cornerRadius: 8,
-      padding: {
-        left: 8,
-        right: 8,
-        top: 4,
-        bottom: 4,
+    container: baseContainer,
+    infoContainer: {
+      ...baseContainer,
+      background: backgroundColor(theme, "on500Info"),
+      border: {
+        color: theme.ramps.blue(0).hex(),
+        width: 1,
       },
-      shadow: popoverShadow(theme),
-      border: border(theme, "primary"),
-      margin: {
-        left: -8,
+    },
+    warningContainer: {
+      ...baseContainer,
+      background: backgroundColor(theme, "on500Warning"),
+      border: {
+        color: theme.ramps.yellow(0).hex(),
+        width: 1,
       },
     },
+    errorContainer: {
+      ...baseContainer,
+      background: backgroundColor(theme, "on500Error"),
+      border: {
+        color: theme.ramps.red(0).hex(),
+        width: 1,
+      }
+    },
     block_style: {
       padding: { top: 4 },
     },

styles/src/themes/common/base16.ts 🔗

@@ -88,16 +88,31 @@ export function createTheme(
       hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
       active: withOpacity(sample(ramps.red, 0.5), 0.25),
     },
+    on500Error: {
+      base: sample(ramps.red, 0.05),
+      hovered: sample(ramps.red, 0.1),
+      active: sample(ramps.red, 0.15),
+    },
     warning: {
       base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
       hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
       active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
     },
+    on500Warning: {
+      base: sample(ramps.yellow, 0.05),
+      hovered: sample(ramps.yellow, 0.1),
+      active: sample(ramps.yellow, 0.15),
+    },
     info: {
       base: withOpacity(sample(ramps.blue, 0.5), 0.15),
       hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
       active: withOpacity(sample(ramps.blue, 0.5), 0.25),
     },
+    on500Info: {
+      base: sample(ramps.blue, 0.05),
+      hovered: sample(ramps.blue, 0.1),
+      active: sample(ramps.blue, 0.15),
+    },
   };
 
   const borderColor = {
@@ -106,10 +121,10 @@ export function createTheme(
     muted: sample(ramps.neutral, isLight ? 1 : 3),
     active: sample(ramps.neutral, isLight ? 4 : 3),
     onMedia: withOpacity(darkest, 0.1),
-    ok: withOpacity(sample(ramps.green, 0.5), 0.15),
-    error: withOpacity(sample(ramps.red, 0.5), 0.15),
-    warning: withOpacity(sample(ramps.yellow, 0.5), 0.15),
-    info: withOpacity(sample(ramps.blue, 0.5), 0.15),
+    ok: sample(ramps.green, 0.3),
+    error: sample(ramps.red, 0.3),
+    warning: sample(ramps.yellow, 0.3),
+    info: sample(ramps.blue, 0.3),
   };
 
   const textColor = {

styles/src/themes/common/theme.ts 🔗

@@ -79,8 +79,11 @@ export default interface Theme {
     on500: BackgroundColorSet;
     ok: BackgroundColorSet;
     error: BackgroundColorSet;
+    on500Error: BackgroundColorSet;
     warning: BackgroundColorSet;
+    on500Warning: BackgroundColorSet;
     info: BackgroundColorSet;
+    on500Info: BackgroundColorSet;
   };
   borderColor: {
     primary: string;